Compare commits

..

16 Commits

Author SHA1 Message Date
03bcc1a939 2. Zwischenstand
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 15s
Build and Push Docker Images / build-frontend (push) Successful in 2m43s
2026-03-21 22:56:56 +01:00
68b2cbb0ee Zwischenstand 2026-03-21 22:13:19 +01:00
b009ac845f Start UI Change 2026-03-21 21:13:22 +01:00
cfd84b773f Revert "Added missing files"
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 15s
Build and Push Docker Images / build-frontend (push) Successful in 53s
This reverts commit 6c3c318f86.
2026-03-21 17:57:26 +01:00
8038f03406 Added missing files 2026-03-21 17:57:23 +01:00
6c3c318f86 Added missing files 2026-03-21 17:56:39 +01:00
8dfcffc92b Added Repository Changelog
Some checks failed
Build and Push Docker Images / build-backend (push) Successful in 27s
Build and Push Docker Images / build-frontend (push) Failing after 28s
2026-03-21 17:52:01 +01:00
9ecacdab50 Handlebars Util 2026-03-21 17:44:44 +01:00
44fb50b11e Removed non available Entries 2026-03-21 17:44:37 +01:00
23c4d21f44 Added UST Auswertung 2026-03-21 17:44:25 +01:00
6f77bccd85 DB Changes 2026-03-21 17:42:59 +01:00
be336a51ab Changes on Admin Interface 2026-03-21 17:10:03 +01:00
ac2e2fcfe9 Fix for no Files present in tenant 2026-03-21 17:09:38 +01:00
9dbb194c8a Fix False Open State for cancelled Invoices 2026-03-21 17:08:57 +01:00
0aacb18aaa Fix False Showing Card 2026-03-21 17:07:47 +01:00
e3a1636018 Fix #44 with Handlebars Templates 2026-03-21 17:05:04 +01:00
117 changed files with 6933 additions and 3934 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE "auth_users"
ADD COLUMN "is_admin" boolean NOT NULL DEFAULT false;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "tasks"
ADD COLUMN "dependency_ids" jsonb NOT NULL DEFAULT '[]'::jsonb;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "tenants"
ADD COLUMN "taxEvaluationPeriod" text DEFAULT 'monthly' NOT NULL;

View File

@@ -148,6 +148,20 @@
"when": 1773835200000, "when": 1773835200000,
"tag": "0021_admin_user_flag", "tag": "0021_admin_user_flag",
"breakpoints": true "breakpoints": true
},
{
"idx": 21,
"version": "7",
"when": 1773925200000,
"tag": "0022_task_dependencies",
"breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1774080000000,
"tag": "0023_tax_evaluation_period",
"breakpoints": true
} }
] ]
} }

View File

@@ -161,6 +161,10 @@ export const tenants = pgTable(
.notNull() .notNull()
.default(14), .default(14),
taxEvaluationPeriod: text("taxEvaluationPeriod")
.notNull()
.default("monthly"),
dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]), dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]),
dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(), dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(),

View File

@@ -1,6 +1,5 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import quarterOfYear from "dayjs/plugin/quarterOfYear"; import quarterOfYear from "dayjs/plugin/quarterOfYear";
import Handlebars from "handlebars";
import axios from "axios"; import axios from "axios";
import { eq, inArray, and } from "drizzle-orm"; // Drizzle Operatoren import { eq, inArray, and } from "drizzle-orm"; // Drizzle Operatoren
@@ -10,6 +9,7 @@ import { saveFile } from "../utils/files";
import {FastifyInstance} from "fastify"; import {FastifyInstance} from "fastify";
import {useNextNumberRangeNumber} from "../utils/functions"; import {useNextNumberRangeNumber} from "../utils/functions";
import {createInvoicePDF} from "../utils/pdf"; // Achtung: Muss Node.js Buffer unterstützen! import {createInvoicePDF} from "../utils/pdf"; // Achtung: Muss Node.js Buffer unterstützen!
import { documentTemplateHandlebars } from "../utils/handlebars";
dayjs.extend(quarterOfYear); dayjs.extend(quarterOfYear);
@@ -609,8 +609,8 @@ export function getDocumentDataBackend(
}; };
}; };
const templateStartText = Handlebars.compile(itemInfo.startText || ""); const templateStartText = documentTemplateHandlebars.compile(itemInfo.startText || "");
const templateEndText = Handlebars.compile(itemInfo.endText || ""); const templateEndText = documentTemplateHandlebars.compile(itemInfo.endText || "");
// --- 6. Title Sums Formatting --- // --- 6. Title Sums Formatting ---
let returnTitleSums: Record<string, string> = {}; let returnTitleSums: Record<string, string> = {};

View File

@@ -30,6 +30,7 @@ export default async function adminRoutes(server: FastifyInstance) {
const createTenantSeeds = async (tenantId: number, createdBy: string) => { const createTenantSeeds = async (tenantId: number, createdBy: string) => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const timestamp = new Date();
const insertedTags = await server.db const insertedTags = await server.db
.insert(filetags) .insert(filetags)
@@ -84,77 +85,148 @@ export default async function adminRoutes(server: FastifyInstance) {
const deliveryTag = insertedTags.find((tag) => tag.createdDocumentType === "deliveryNotes"); const deliveryTag = insertedTags.find((tag) => tag.createdDocumentType === "deliveryNotes");
const incomingInvoiceTag = insertedTags.find((tag) => tag.incomingDocumentType === "invoices"); const incomingInvoiceTag = insertedTags.find((tag) => tag.incomingDocumentType === "invoices");
await server.db const insertedFolders = await server.db
.insert(folders) .insert(folders)
.values([ .values([
{ {
tenant: tenantId, tenant: tenantId,
name: "Ausgangsrechnungen", name: "Ausgangsrechnungen",
function: "yearSubCategory",
icon: "i-heroicons-document-text",
isSystemUsed: true,
updatedAt: timestamp,
updatedBy: createdBy,
},
{
tenant: tenantId,
name: "Angebote",
function: "yearSubCategory",
icon: "i-heroicons-document-duplicate",
isSystemUsed: true,
updatedAt: timestamp,
updatedBy: createdBy,
},
{
tenant: tenantId,
name: "Auftragsbestätigungen",
function: "yearSubCategory",
icon: "i-heroicons-clipboard-document-check",
isSystemUsed: true,
updatedAt: timestamp,
updatedBy: createdBy,
},
{
tenant: tenantId,
name: "Lieferscheine",
function: "yearSubCategory",
icon: "i-heroicons-truck",
isSystemUsed: true,
updatedAt: timestamp,
updatedBy: createdBy,
},
{
tenant: tenantId,
name: "Eingangsrechnungen",
function: "yearSubCategory",
icon: "i-heroicons-inbox-arrow-down",
isSystemUsed: true,
updatedAt: timestamp,
updatedBy: createdBy,
},
{
tenant: tenantId,
name: "Belege Bankeinzahlung",
function: "yearSubCategory",
icon: "i-heroicons-banknotes",
isSystemUsed: true,
updatedAt: timestamp,
updatedBy: createdBy,
},
])
.returning({
id: folders.id,
name: folders.name,
});
const folderByName = new Map(insertedFolders.map((folder) => [folder.name, folder.id]));
await server.db
.insert(folders)
.values([
{
tenant: tenantId,
name: String(currentYear),
parent: folderByName.get("Ausgangsrechnungen"),
function: "invoices", function: "invoices",
year: currentYear, year: currentYear,
icon: "i-heroicons-document-text", icon: "i-heroicons-document-text",
standardFiletype: invoiceTag?.id, standardFiletype: invoiceTag?.id,
standardFiletypeIsOptional: false, standardFiletypeIsOptional: false,
isSystemUsed: true, isSystemUsed: true,
updatedAt: new Date(), updatedAt: timestamp,
updatedBy: createdBy, updatedBy: createdBy,
}, },
{ {
tenant: tenantId, tenant: tenantId,
name: "Angebote", name: String(currentYear),
parent: folderByName.get("Angebote"),
function: "quotes", function: "quotes",
year: currentYear, year: currentYear,
icon: "i-heroicons-document-duplicate", icon: "i-heroicons-document-duplicate",
standardFiletype: quoteTag?.id, standardFiletype: quoteTag?.id,
standardFiletypeIsOptional: false, standardFiletypeIsOptional: false,
isSystemUsed: true, isSystemUsed: true,
updatedAt: new Date(), updatedAt: timestamp,
updatedBy: createdBy, updatedBy: createdBy,
}, },
{ {
tenant: tenantId, tenant: tenantId,
name: "Auftragsbestätigungen", name: String(currentYear),
parent: folderByName.get("Auftragsbestätigungen"),
function: "confirmationOrders", function: "confirmationOrders",
year: currentYear, year: currentYear,
icon: "i-heroicons-clipboard-document-check", icon: "i-heroicons-clipboard-document-check",
standardFiletype: confirmationTag?.id, standardFiletype: confirmationTag?.id,
standardFiletypeIsOptional: false, standardFiletypeIsOptional: false,
isSystemUsed: true, isSystemUsed: true,
updatedAt: new Date(), updatedAt: timestamp,
updatedBy: createdBy, updatedBy: createdBy,
}, },
{ {
tenant: tenantId, tenant: tenantId,
name: "Lieferscheine", name: String(currentYear),
parent: folderByName.get("Lieferscheine"),
function: "deliveryNotes", function: "deliveryNotes",
year: currentYear, year: currentYear,
icon: "i-heroicons-truck", icon: "i-heroicons-truck",
standardFiletype: deliveryTag?.id, standardFiletype: deliveryTag?.id,
standardFiletypeIsOptional: false, standardFiletypeIsOptional: false,
isSystemUsed: true, isSystemUsed: true,
updatedAt: new Date(), updatedAt: timestamp,
updatedBy: createdBy, updatedBy: createdBy,
}, },
{ {
tenant: tenantId, tenant: tenantId,
name: "Eingangsrechnungen", name: String(currentYear),
parent: folderByName.get("Eingangsrechnungen"),
function: "incomingInvoices", function: "incomingInvoices",
year: currentYear, year: currentYear,
icon: "i-heroicons-inbox-arrow-down", icon: "i-heroicons-inbox-arrow-down",
standardFiletype: incomingInvoiceTag?.id, standardFiletype: incomingInvoiceTag?.id,
standardFiletypeIsOptional: false, standardFiletypeIsOptional: false,
isSystemUsed: true, isSystemUsed: true,
updatedAt: new Date(), updatedAt: timestamp,
updatedBy: createdBy, updatedBy: createdBy,
}, },
{ {
tenant: tenantId, tenant: tenantId,
name: "Belege Bankeinzahlung", name: String(currentYear),
parent: folderByName.get("Belege Bankeinzahlung"),
function: "deposit", function: "deposit",
year: currentYear, year: currentYear,
icon: "i-heroicons-banknotes", icon: "i-heroicons-banknotes",
isSystemUsed: true, isSystemUsed: true,
updatedAt: new Date(), updatedAt: timestamp,
updatedBy: createdBy, updatedBy: createdBy,
}, },
]); ]);

View File

@@ -57,6 +57,7 @@ export default async function meRoutes(server: FastifyInstance) {
businessInfo: tenants.businessInfo, businessInfo: tenants.businessInfo,
numberRanges: tenants.numberRanges, numberRanges: tenants.numberRanges,
accountChart: tenants.accountChart, accountChart: tenants.accountChart,
taxEvaluationPeriod: tenants.taxEvaluationPeriod,
dokuboxkey: tenants.dokuboxkey, dokuboxkey: tenants.dokuboxkey,
standardEmailForInvoices: tenants.standardEmailForInvoices, standardEmailForInvoices: tenants.standardEmailForInvoices,
standardPaymentDays: tenants.standardPaymentDays, standardPaymentDays: tenants.standardPaymentDays,

View File

@@ -237,7 +237,7 @@ export default async function fileRoutes(server: FastifyInstance) {
// MULTIPLE PRESIGNED URLs // MULTIPLE PRESIGNED URLs
// ------------------------------------------------- // -------------------------------------------------
if (!ids || !Array.isArray(ids) || ids.length === 0) { if (!ids || !Array.isArray(ids) || ids.length === 0) {
return reply.code(400).send({ error: "No ids provided" }) return { files: [] }
} }
const rows = await server.db const rows = await server.db

View File

@@ -2,6 +2,10 @@ import { FastifyInstance } from "fastify";
import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf"; import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf";
import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions"; import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions";
import { GetObjectCommand } from "@aws-sdk/client-s3"; import { GetObjectCommand } from "@aws-sdk/client-s3";
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import dayjs from "dayjs"; import dayjs from "dayjs";
//import { ready as zplReady } from 'zpl-renderer-js' //import { ready as zplReady } from 'zpl-renderer-js'
//import { renderZPL } from "zpl-image"; //import { renderZPL } from "zpl-image";
@@ -28,6 +32,31 @@ dayjs.extend(isSameOrBefore)
dayjs.extend(duration) dayjs.extend(duration)
dayjs.extend(timezone) dayjs.extend(timezone)
const execFileAsync = promisify(execFile)
function resolveGitRoot() {
const searchRoots = [
process.cwd(),
path.resolve(process.cwd(), ".."),
path.resolve(__dirname, "../../.."),
path.resolve(__dirname, "../../../.."),
]
for (const startDir of searchRoots) {
let currentDir = startDir
while (currentDir && currentDir !== path.dirname(currentDir)) {
if (existsSync(path.join(currentDir, ".git"))) {
return currentDir
}
currentDir = path.dirname(currentDir)
}
}
return null
}
export default async function functionRoutes(server: FastifyInstance) { export default async function functionRoutes(server: FastifyInstance) {
const streamToBuffer = async (stream: any): Promise<Buffer> => const streamToBuffer = async (stream: any): Promise<Buffer> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@@ -162,6 +191,55 @@ export default async function functionRoutes(server: FastifyInstance) {
} }
}) })
server.get('/functions/changelog', async (req, reply) => {
const { limit } = req.query as { limit?: string | number }
const parsedLimit = Number(limit)
const safeLimit = Number.isFinite(parsedLimit)
? Math.min(Math.max(parsedLimit, 1), 50)
: 15
const gitRoot = resolveGitRoot()
if (!gitRoot) {
return reply.code(500).send({ error: 'Git repository not found' })
}
try {
const { stdout } = await execFileAsync('git', [
'-C',
gitRoot,
'log',
`--max-count=${safeLimit}`,
'--date=iso-strict',
'--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1e'
])
const entries = stdout
.split('\x1e')
.map(entry => entry.trim())
.filter(Boolean)
.map(entry => {
const [hash, shortHash, subject, authorName, committedAt] = entry.split('\x1f')
return {
hash,
shortHash,
subject,
authorName,
committedAt
}
})
return reply.send({
repositoryRoot: gitRoot,
entries
})
} catch (err) {
req.log.error(err)
return reply.code(500).send({ error: 'Failed to load changelog' })
}
})
server.post('/functions/serial/start', async (req, reply) => { server.post('/functions/serial/start', async (req, reply) => {
console.log(req.body) console.log(req.body)
const {executionDate,templateIds,tenantId} = req.body as {executionDate:string,templateIds:Number[],tenantId:Number} const {executionDate,templateIds,tenantId} = req.body as {executionDate:string,templateIds:Number[],tenantId:Number}

View File

@@ -0,0 +1,26 @@
import Handlebars from "handlebars";
const createDocumentTemplateHandlebars = () => {
const instance = Handlebars.create();
instance.registerHelper("eq", (left, right) => left === right);
instance.registerHelper("ne", (left, right) => left !== right);
instance.registerHelper("gt", (left, right) => left > right);
instance.registerHelper("gte", (left, right) => left >= right);
instance.registerHelper("lt", (left, right) => left < right);
instance.registerHelper("lte", (left, right) => left <= right);
instance.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean));
instance.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean));
instance.registerHelper("not", (value) => !value);
instance.registerHelper("includes", (collection, value) => {
if (Array.isArray(collection) || typeof collection === "string") {
return collection.includes(value);
}
return false;
});
return instance;
};
export const documentTemplateHandlebars = createDocumentTemplateHandlebars();

View File

@@ -1,7 +1,9 @@
export default defineAppConfig({ export default defineAppConfig({
ui: { ui: {
primary: 'green', colors: {
gray: 'slate', primary: 'green',
neutral: 'slate'
},
tooltip: { tooltip: {
background: '!bg-background' background: '!bg-background'
}, },
@@ -35,4 +37,4 @@ export default defineAppConfig({
} }
} }
} }
}) })

View File

@@ -47,14 +47,16 @@ useSeoMeta({
</script> </script>
<template> <template>
<div class="safearea"> <UApp>
<NuxtLayout> <div class="safearea">
<NuxtPage/> <NuxtLayout>
</NuxtLayout> <NuxtPage/>
<UNotifications/> </NuxtLayout>
<USlideovers /> <UNotifications/>
<UModals/> <USlideovers />
</div> <UModals/>
</div>
</UApp>
@@ -136,4 +138,4 @@ useSeoMeta({
.scroll { .scroll {
overflow-y: scroll; overflow-y: scroll;
} }
</style> </style>

View 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);
}

View File

@@ -38,37 +38,39 @@ const emitConfirm = () => {
> >
Archivieren Archivieren
</UButton> </UButton>
<UModal v-model="showModal"> <UModal v-model:open="showModal">
<UCard> <template #content>
<template #header> <UCard>
<span class="text-md font-bold">Archivieren bestätigen</span> <template #header>
</template> <span class="text-md font-bold">Archivieren bestätigen</span>
Möchten Sie diese/-s/-n {{dataType.labelSingle}} wirklich archivieren? </template>
Möchten Sie diese/-s/-n {{dataType.labelSingle}} wirklich archivieren?
<template #footer> <template #footer>
<div class="text-right"> <div class="text-right">
<UButtonGroup> <UButtonGroup>
<UButton <UButton
variant="outline" variant="outline"
@click="showModal = false" @click="showModal = false"
> >
Abbrechen Abbrechen
</UButton> </UButton>
<UButton <UButton
@click="emitConfirm" @click="emitConfirm"
class="ml-2" class="ml-2"
color="rose" color="error"
> >
Archivieren Archivieren
</UButton> </UButton>
</UButtonGroup> </UButtonGroup>
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@@ -49,7 +49,7 @@ const assignByIban = async () => {
const match = accounts.value.find((a) => normalizeIban(a.iban) === search) const match = accounts.value.find((a) => normalizeIban(a.iban) === search)
if (!match) { 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 return
} }
@@ -68,7 +68,7 @@ const removeAssigned = (id) => {
const createAndAssign = async () => { const createAndAssign = async () => {
if (!createPayload.value.iban || !createPayload.value.bic || !createPayload.value.bankName) { 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 return
} }
@@ -140,43 +140,45 @@ loadAccounts()
</InputGroup> </InputGroup>
</div> </div>
<UModal v-model="showCreate"> <UModal v-model:open="showCreate">
<UCard> <template #content>
<template #header>Neue Bankverbindung erstellen</template> <UCard>
<div class="space-y-3"> <template #header>Neue Bankverbindung erstellen</template>
<UFormGroup label="IBAN"> <div class="space-y-3">
<InputGroup> <UFormField label="IBAN">
<UInput <InputGroup>
v-model="createPayload.iban" <UInput
@blur="resolveCreatePayloadFromIban" v-model="createPayload.iban"
@keydown.enter.prevent="resolveCreatePayloadFromIban" @blur="resolveCreatePayloadFromIban"
/> @keydown.enter.prevent="resolveCreatePayloadFromIban"
<UButton />
color="gray" <UButton
variant="outline" color="gray"
:loading="resolvingIban" variant="outline"
@click="resolveCreatePayloadFromIban" :loading="resolvingIban"
> @click="resolveCreatePayloadFromIban"
Ermitteln >
</UButton> Ermitteln
</InputGroup> </UButton>
</UFormGroup> </InputGroup>
<UFormGroup label="BIC"> </UFormField>
<UInput v-model="createPayload.bic" /> <UFormField label="BIC">
</UFormGroup> <UInput v-model="createPayload.bic" />
<UFormGroup label="Bankinstitut"> </UFormField>
<UInput v-model="createPayload.bankName" /> <UFormField label="Bankinstitut">
</UFormGroup> <UInput v-model="createPayload.bankName" />
<UFormGroup label="Beschreibung (optional)"> </UFormField>
<UInput v-model="createPayload.description" /> <UFormField label="Beschreibung (optional)">
</UFormGroup> <UInput v-model="createPayload.description" />
</div> </UFormField>
<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> </div>
</template> <template #footer>
</UCard> <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> </UModal>
</template> </template>

View File

@@ -31,36 +31,38 @@ const emitConfirm = () => {
> >
<slot name="button"></slot> <slot name="button"></slot>
</UButton> </UButton>
<UModal v-model="showModal"> <UModal v-model:open="showModal">
<UCard> <template #content>
<template #header> <UCard>
<slot name="header"></slot> <template #header>
</template> <slot name="header"></slot>
<slot/> </template>
<template #footer> <slot/>
<div class="text-right"> <template #footer>
<UButtonGroup> <div class="text-right">
<UButton <UButtonGroup>
variant="outline" <UButton
@click="showModal = false" variant="outline"
> @click="showModal = false"
Abbrechen >
</UButton> Abbrechen
<UButton </UButton>
@click="emitConfirm" <UButton
class="ml-2" @click="emitConfirm"
color="rose" class="ml-2"
> color="error"
Archivieren >
</UButton> Archivieren
</UButtonGroup> </UButton>
</UButtonGroup>
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@@ -51,8 +51,8 @@
<div class="grid grid-cols-4 gap-2"> <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 (+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="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 (-19%)"><UButton color="error" 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 (-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="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> <UTooltip text="Speicher +"><UButton color="gray" variant="ghost" block @click="addToSum">M+</UButton></UTooltip>
@@ -227,9 +227,14 @@ defineShortcuts({
width: 4px; width: 4px;
} }
.custom-scrollbar::-webkit-scrollbar-track { .custom-scrollbar::-webkit-scrollbar-track {
@apply bg-transparent; background: transparent;
} }
.custom-scrollbar::-webkit-scrollbar-thumb { .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>

View File

@@ -156,7 +156,8 @@ const moveFile = async () => {
<template> <template>
<UModal fullscreen > <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> <template #header>
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -186,7 +187,7 @@ const moveFile = async () => {
<div class="w-2/3 p-5" v-if="!false"> <div class="w-2/3 p-5" v-if="!false">
<UButtonGroup> <UButtonGroup>
<ArchiveButton <ArchiveButton
color="rose" color="error"
variant="outline" variant="outline"
type="files" type="files"
@confirmed="archiveDocument" @confirmed="archiveDocument"
@@ -202,7 +203,7 @@ const moveFile = async () => {
</UButton> </UButton>
</UButtonGroup> </UButtonGroup>
<UDivider>Zuweisungen</UDivider> <USeparator label="Zuweisungen"/>
<table class="w-full"> <table class="w-full">
<tr v-if="props.documentData.project"> <tr v-if="props.documentData.project">
<td>Projekt</td> <td>Projekt</td>
@@ -278,44 +279,44 @@ const moveFile = async () => {
</tr> </tr>
</table> </table>
<UDivider class="my-3">Datei zuweisen</UDivider> <USeparator class="my-3" label="Datei zuweisen"/>
<UFormGroup <UFormField
label="Resource auswählen" label="Resource auswählen"
> >
<USelectMenu <USelectMenu
:options="resourceOptions" :items="resourceOptions"
v-model="resourceToAssign" v-model="resourceToAssign"
value-attribute="value" value-key="value"
option-attribute="label" label-key="label"
@change="getItemsBySelectedResource" @change="getItemsBySelectedResource"
> >
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Eintrag auswählen:" label="Eintrag auswählen:"
> >
<USelectMenu <USelectMenu
:options="itemOptions" :items="itemOptions"
v-model="idToAssign" v-model="idToAssign"
:option-attribute="resourceOptions.find(i => i.value === resourceToAssign)? resourceOptions.find(i => i.value === resourceToAssign).optionAttr : 'name'" :label-key="resourceOptions.find(i => i.value === resourceToAssign)? resourceOptions.find(i => i.value === resourceToAssign).optionAttr : 'name'"
value-attribute="id" value-key="id"
@change="updateDocumentAssignment" @change="updateDocumentAssignment"
></USelectMenu> ></USelectMenu>
</UFormGroup> </UFormField>
<UDivider class="my-5">Datei verschieben</UDivider> <USeparator class="my-5" label="Datei verschieben"/>
<InputGroup class="w-full"> <InputGroup class="w-full">
<USelectMenu <USelectMenu
class="flex-auto" class="flex-auto"
v-model="folderToMoveTo" v-model="folderToMoveTo"
value-attribute="id" value-key="id"
option-attribute="name" label-key="name"
:options="folders" :items="folders"
/> />
<UButton <UButton
@click="moveFile" @click="moveFile"
@@ -324,34 +325,35 @@ const moveFile = async () => {
>Verschieben</UButton> >Verschieben</UButton>
</InputGroup> </InputGroup>
<UDivider class="my-5">Dateityp</UDivider> <USeparator class="my-5" label="Dateityp"/>
<InputGroup class="w-full"> <InputGroup class="w-full">
<USelectMenu <USelectMenu
class="flex-auto" class="flex-auto"
v-model="props.documentData.type" v-model="props.documentData.type"
value-attribute="id" value-key="id"
option-attribute="name" label-key="name"
:options="filetypes" :items="filetypes"
@change="updateDocument" @change="updateDocument"
/> />
</InputGroup> </InputGroup>
<UDivider class="my-5">Dokumentenbox</UDivider> <USeparator class="my-5" label="Dokumentenbox" />
<InputGroup class="w-full"> <InputGroup class="w-full">
<USelectMenu <USelectMenu
class="flex-auto" class="flex-auto"
v-model="props.documentData.documentbox" v-model="props.documentData.documentbox"
value-attribute="id" value-key="id"
option-attribute="key" label-key="key"
:options="documentboxes" :items="documentboxes"
@change="updateDocument" @change="updateDocument"
/> />
</InputGroup> </InputGroup>
</div> </div>
</div> </div>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>
@@ -362,4 +364,4 @@ const moveFile = async () => {
aspect-ratio: 1/ 1.414; aspect-ratio: 1/ 1.414;
} }
</style> </style>

View File

@@ -78,84 +78,86 @@ const fileNames = computed(() => {
<template> <template>
<UModal> <UModal>
<div ref="dropZoneRef" class="relative h-full flex flex-col"> <template #content>
<div ref="dropZoneRef" class="relative h-full flex flex-col">
<div <div
v-if="isOverDropZone" 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" 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'"
> >
<UInput <span class="text-xl font-bold text-primary-600 bg-white/80 px-4 py-2 rounded shadow-sm">
v-if="selectedFiles.length === 0" Dateien hier ablegen
type="file" </span>
id="fileUploadInput" </div>
multiple
accept="image/jpeg, image/png, image/gif, application/pdf"
@change="onFileInputChange"
/>
<div v-if="selectedFiles.length > 0" class="mt-2 text-sm text-gray-500"> <UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
Ausgewählt: <span class="font-medium text-gray-700 dark:text-gray-300">{{ fileNames }}</span> <template #header>
</div> <div class="flex items-center justify-between">
</UFormGroup> <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 <UFormField
label="Typ:" label="Datei:"
class="mt-3" :help="selectedFiles.length > 0 ? `${selectedFiles.length} Datei(en) ausgewählt` : 'Ziehen Sie Dateien hierher oder klicken Sie'"
>
<USelectMenu
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suchen..."
:options="availableFiletypes"
v-model="props.fileData.type"
:disabled="!props.fileData.typeEnabled"
> >
<template #label> <UInput
<span v-if="availableFiletypes.find(x => x.id === props.fileData.type)">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span> v-if="selectedFiles.length === 0"
<span v-else>Kein Typ ausgewählt</span> type="file"
</template> id="fileUploadInput"
</USelectMenu> multiple
</UFormGroup> accept="image/jpeg, image/png, image/gif, application/pdf"
@change="onFileInputChange"
/>
<template #footer> <div v-if="selectedFiles.length > 0" class="mt-2 text-sm text-gray-500">
<UButton Ausgewählt: <span class="font-medium text-gray-700 dark:text-gray-300">{{ fileNames }}</span>
@click="uploadFiles" </div>
:loading="uploadInProgress" </UFormField>
:disabled="uploadInProgress || selectedFiles.length === 0"
>Hochladen</UButton> <UFormField
</template> label="Typ:"
</UCard> class="mt-3"
</div> >
<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> </UModal>
</template> </template>
<style scoped> <style scoped>
/* Optional: Animationen für das Overlay */ /* Optional: Animationen für das Overlay */
</style> </style>

View File

@@ -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 () => { const createItem = async () => {
let ret = null let ret = null
@@ -264,7 +280,7 @@ const updateItem = async () => {
</template> </template>
<template #right> <template #right>
<ArchiveButton <ArchiveButton
color="rose" color="error"
v-if="platform !== 'mobile'" v-if="platform !== 'mobile'"
variant="outline" variant="outline"
:type="type" :type="type"
@@ -336,12 +352,12 @@ const updateItem = async () => {
v-for="(columnName,index) in dataType.inputColumns" v-for="(columnName,index) in dataType.inputColumns"
:class="platform === 'mobile' ? ['w-full'] : [`w-1/${dataType.inputColumns.length}`, ... index < dataType.inputColumns.length -1 ? ['mr-5'] : []]" :class="platform === 'mobile' ? ['w-full'] : [`w-1/${dataType.inputColumns.length}`, ... index < dataType.inputColumns.length -1 ? ['mr-5'] : []]"
> >
<UDivider>{{ columnName }}</UDivider> <USeparator :label="columnName"/>
<div <div
v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && i.inputColumn === columnName)" v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && i.inputColumn === columnName)"
> >
<UFormGroup <UFormField
v-if="(datapoint.showFunction ? datapoint.showFunction(item) : true)" v-if="(datapoint.showFunction ? datapoint.showFunction(item) : true)"
:label="datapoint.label" :label="datapoint.label"
> >
@@ -354,7 +370,7 @@ const updateItem = async () => {
</template> </template>
<InputGroup class="w-full" v-if="datapoint.key.includes('.')"> <InputGroup class="w-full" v-if="datapoint.key.includes('.')">
<UInput <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" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)" 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> <span class="text-gray-500 dark:text-gray-400 text-xs">{{ datapoint.inputTrailing }}</span>
</template> </template>
</UInput> </UInput>
<UToggle <USwitch
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'primary'" :color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'primary'"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'" v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<USelectMenu <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" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'" v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute" :items="getSelectItems(datapoint)"
:value-attribute="datapoint.selectValueAttribute || 'id'" :label-key="getSelectLabelKey(datapoint)"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]" :value-key="getSelectValueKey(datapoint)"
:searchable="datapoint.selectSearchAttributes" :search-input="getSelectSearchInput(datapoint)"
:search-attributes="datapoint.selectSearchAttributes" :filter-fields="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple" :multiple="datapoint.selectMultiple"
> >
<template #empty> <template #empty>
@@ -393,7 +409,7 @@ const updateItem = async () => {
</template> </template>
</USelectMenu> </USelectMenu>
<UTextarea <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" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'" v-else-if="datapoint.inputType === 'textarea'"
@@ -401,9 +417,9 @@ const updateItem = async () => {
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4" 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 <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" 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'" :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" variant="outline"
@@ -411,17 +427,17 @@ const updateItem = async () => {
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @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" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
</template> </template>
</UPopover> </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 <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" 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'" :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" variant="outline"
@@ -429,10 +445,10 @@ const updateItem = async () => {
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @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" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime" mode="datetime"
/> />
@@ -460,7 +476,7 @@ const updateItem = async () => {
<InputGroup class="w-full" v-else> <InputGroup class="w-full" v-else>
<UInput <UInput
class="flex-auto" 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" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)" v-if="['text','number'].includes(datapoint.inputType)"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
@@ -472,34 +488,33 @@ const updateItem = async () => {
{{ datapoint.inputTrailing }} {{ datapoint.inputTrailing }}
</template> </template>
</UInput> </UInput>
<UToggle <USwitch
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'primary'" :color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'primary'"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'" v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<USelectMenu <USelectMenu
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'" :color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
class="flex-auto" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'" v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute" :items="getSelectItems(datapoint)"
:value-attribute="datapoint.selectValueAttribute || 'id'" :label-key="getSelectLabelKey(datapoint)"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]" :value-key="getSelectValueKey(datapoint)"
:searchable="datapoint.selectSearchAttributes" :search-input="getSelectSearchInput(datapoint)"
:search-attributes="datapoint.selectSearchAttributes" :filter-fields="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple" :multiple="datapoint.selectMultiple"
searchable-placeholder="Suche..."
> >
<template #empty> <template #empty>
Keine Optionen verfügbar Keine Optionen verfügbar
</template> </template>
</USelectMenu> </USelectMenu>
<UTextarea <UTextarea
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'" :color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
class="flex-auto" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'" v-else-if="datapoint.inputType === 'textarea'"
@@ -507,37 +522,36 @@ const updateItem = async () => {
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4" 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 <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" icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'" :label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline" variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @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" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
</template> </template>
</UPopover> </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 <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" icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'" :label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
variant="outline" variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
@close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime" mode="datetime"
/> />
@@ -572,11 +586,11 @@ const updateItem = async () => {
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
/> />
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
</div> </div>
</div> </div>
</div> </div>
<UFormGroup <UFormField
v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && !i.inputColumn)" v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && !i.inputColumn)"
:label="datapoint.label" :label="datapoint.label"
> >
@@ -589,7 +603,7 @@ const updateItem = async () => {
</template> </template>
<InputGroup class="w-full" v-if="datapoint.key.includes('.')"> <InputGroup class="w-full" v-if="datapoint.key.includes('.')">
<UInput <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" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)" 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> <span class="text-gray-500 dark:text-gray-400 text-xs">{{ datapoint.inputTrailing }}</span>
</template> </template>
</UInput> </UInput>
<UToggle <USwitch
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'primary'" :color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'primary'"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'" v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<USelectMenu <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" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'" v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute" :items="getSelectItems(datapoint)"
:value-attribute="datapoint.selectValueAttribute || 'id'" :label-key="getSelectLabelKey(datapoint)"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]" :value-key="getSelectValueKey(datapoint)"
:searchable="datapoint.selectSearchAttributes" :search-input="getSelectSearchInput(datapoint)"
:search-attributes="datapoint.selectSearchAttributes" :filter-fields="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple" :multiple="datapoint.selectMultiple"
> >
<template #empty> <template #empty>
@@ -628,7 +642,7 @@ const updateItem = async () => {
</template> </template>
</USelectMenu> </USelectMenu>
<UTextarea <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" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'" v-else-if="datapoint.inputType === 'textarea'"
@@ -636,9 +650,9 @@ const updateItem = async () => {
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4" 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 <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" 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'" :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" variant="outline"
@@ -646,17 +660,17 @@ const updateItem = async () => {
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @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" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
</template> </template>
</UPopover> </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 <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" 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'" :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" variant="outline"
@@ -664,10 +678,10 @@ const updateItem = async () => {
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @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" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime" mode="datetime"
/> />
@@ -695,7 +709,7 @@ const updateItem = async () => {
<InputGroup class="w-full" v-else> <InputGroup class="w-full" v-else>
<UInput <UInput
class="flex-auto" 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" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)" v-if="['text','number'].includes(datapoint.inputType)"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
@@ -707,34 +721,33 @@ const updateItem = async () => {
{{ datapoint.inputTrailing }} {{ datapoint.inputTrailing }}
</template> </template>
</UInput> </UInput>
<UToggle <USwitch
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'primary'" :color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'primary'"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'" v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<USelectMenu <USelectMenu
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'" :color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
class="flex-auto" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'" v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute" :items="getSelectItems(datapoint)"
:value-attribute="datapoint.selectValueAttribute || 'id'" :label-key="getSelectLabelKey(datapoint)"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]" :value-key="getSelectValueKey(datapoint)"
:searchable="datapoint.selectSearchAttributes" :search-input="getSelectSearchInput(datapoint)"
:search-attributes="datapoint.selectSearchAttributes" :filter-fields="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple" :multiple="datapoint.selectMultiple"
searchable-placeholder="Suche..."
> >
<template #empty> <template #empty>
Keine Optionen verfügbar Keine Optionen verfügbar
</template> </template>
</USelectMenu> </USelectMenu>
<UTextarea <UTextarea
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'" :color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
class="flex-auto" class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'" v-else-if="datapoint.inputType === 'textarea'"
@@ -742,37 +755,36 @@ const updateItem = async () => {
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4" 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 <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" icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'" :label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline" variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @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" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
</template> </template>
</UPopover> </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 <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" icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'" :label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
variant="outline" variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/> />
<template #panel="{ close }"> <template #content>
<LazyDatePicker <LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null" @change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key]" v-model="item[datapoint.key]"
@close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false" :disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime" mode="datetime"
/> />
@@ -807,7 +819,7 @@ const updateItem = async () => {
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
/> />
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
</UForm> </UForm>
</UDashboardPanelContent> </UDashboardPanelContent>
</template> </template>

View File

@@ -110,12 +110,6 @@ const filteredRows = computed(() => {
</script> </script>
<template> <template>
<FloatingActionButton
:label="`+ ${dataType.labelSingle}`"
variant="outline"
v-if="platform === 'mobile'"
@click="router.push(`/standardEntity/${type}/create`)"
/>
<UDashboardNavbar :title="dataType.label" :badge="filteredRows.length"> <UDashboardNavbar :title="dataType.label" :badge="filteredRows.length">
<template #toggle> <template #toggle>
<div v-if="platform === 'mobile'"></div> <div v-if="platform === 'mobile'"></div>
@@ -138,7 +132,7 @@ const filteredRows = computed(() => {
<UButton <UButton
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
variant="outline" variant="outline"
color="rose" color="error"
@click="clearSearchString()" @click="clearSearchString()"
v-if="searchString.length > 0" v-if="searchString.length > 0"
/> />
@@ -161,15 +155,15 @@ const filteredRows = computed(() => {
<USelectMenu <USelectMenu
v-model="selectedColumns" v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
:options="dataType.templateColumns.filter(i => !i.disabledInTable)" :items="dataType.templateColumns.filter(i => !i.disabledInTable)"
multiple multiple
class="hidden lg:block" class="hidden lg:block"
by="key" by="key"
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'" :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)" @change="tempStore.modifyColumns(type,selectedColumns)"
> >
<template #label> <template #default>
Spalten Spalten
</template> </template>
</USelectMenu> </USelectMenu>
@@ -178,11 +172,11 @@ const filteredRows = computed(() => {
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
multiple multiple
v-model="selectedFilters" v-model="selectedFilters"
:options="selectableFilters" :items="selectableFilters"
:color="selectedFilters.length > 0 ? 'primary' : 'white'" :color="selectedFilters.length > 0 ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }" :content="{ width: 'min-w-max' }"
> >
<template #label> <template #default>
Filter Filter
</template> </template>
</USelectMenu> </USelectMenu>
@@ -191,14 +185,14 @@ const filteredRows = computed(() => {
<EntityTableMobile <EntityTableMobile
v-if="platform === 'mobile'" v-if="platform === 'mobile'"
:type="props.type" :type="props.type"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="filteredRows" :rows="filteredRows"
/> />
<EntityTable <EntityTable
v-else v-else
@sort="(i) => emit('sort',i)" @sort="(i) => emit('sort',i)"
:type="props.type" :type="props.type"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="filteredRows" :rows="filteredRows"
:loading="props.loading" :loading="props.loading"
/> />

View File

@@ -28,14 +28,16 @@ defineShortcuts({
router.back() router.back()
}, },
'arrowleft': () => { 'arrowleft': () => {
if(openTab.value > 0){ const currentIndex = Number(openTab.value)
openTab.value -= 1 if(currentIndex > 0){
openTab.value = String(currentIndex - 1)
router.push(`${router.currentRoute.value.path}?tabIndex=${openTab.value}`) router.push(`${router.currentRoute.value.path}?tabIndex=${openTab.value}`)
} }
}, },
'arrowright': () => { 'arrowright': () => {
if(openTab.value < dataType.showTabs.length - 1) { const currentIndex = Number(openTab.value)
openTab.value += 1 if(currentIndex < dataType.showTabs.length - 1) {
openTab.value = String(currentIndex + 1)
router.push(`${router.currentRoute.value.path}?tabIndex=${openTab.value}`) router.push(`${router.currentRoute.value.path}?tabIndex=${openTab.value}`)
} }
}, },
@@ -51,7 +53,7 @@ const auth = useAuthStore()
const dataType = dataStore.dataTypes[type] 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) => { 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 () => { const changePinned = async () => {
@@ -255,9 +258,9 @@ const openCustomerInventoryLabelPrint = () => {
v-if="props.item.id && platform !== 'mobile'" v-if="props.item.id && platform !== 'mobile'"
class="p-5" class="p-5"
v-model="openTab" 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"> <div v-if="tab.label === 'Informationen'" class="flex flex-row">
<EntityShowSubInformation <EntityShowSubInformation

View File

@@ -96,15 +96,15 @@ setup()
<USelectMenu <USelectMenu
v-model="selectedColumns" v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
:options="dataType.templateColumns.filter(i => !i.disabledInTable)" :items="dataType.templateColumns.filter(i => !i.disabledInTable)"
multiple multiple
class="hidden lg:block" class="hidden lg:block"
by="key" by="key"
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'" :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)" @change="tempStore.modifyColumns(type,selectedColumns)"
> >
<template #label> <template #default>
Spalten Spalten
</template> </template>
</USelectMenu> </USelectMenu>
@@ -114,7 +114,7 @@ setup()
<div class="scroll" style="height: 70vh"> <div class="scroll" style="height: 70vh">
<EntityTable <EntityTable
:type="type" :type="type"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="props.item[type]" :rows="props.item[type]"
style style
/> />

View File

@@ -181,49 +181,51 @@ const selectItem = (item) => {
</UButton> </UButton>
<UModal <UModal
prevent-close prevent-close
v-model="showFinalInvoiceConfig" v-model:open="showFinalInvoiceConfig"
> >
<UCard> <template #content>
<template #header> <UCard>
<div class="flex items-center justify-between"> <template #header>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <div class="flex items-center justify-between">
Schlussrechnung konfigurieren <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
</h3> Schlussrechnung konfigurieren
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" /> </h3>
</div> <UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showFinalInvoiceConfig = false" />
</template> </div>
</template>
<UFormGroup <UFormField
label="Rechnungsvorlage" 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"
> >
Weiter <USelectMenu
</UButton> :items="props.item.createddocuments.filter(i => ['confirmationOrders','quotes'].includes(i.type))"
</template> value-key="id"
</UCard> 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> </UModal>
<UButton <UButton
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'invoices'})}`)" @click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'invoices'})}`)"
@@ -235,48 +237,48 @@ const selectItem = (item) => {
<USelectMenu <USelectMenu
v-model="selectedColumns" v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns" :items="templateColumns"
multiple multiple
class="hidden lg:block" class="hidden lg:block"
by="key" by="key"
@change="tempStore.modifyColumns('createddocuments',selectedColumns)" @change="tempStore.modifyColumns('createddocuments',selectedColumns)"
> >
<template #label> <template #default>
Spalten Spalten
</template> </template>
</USelectMenu> </USelectMenu>
</template> </template>
</Toolbar> </Toolbar>
<UTable <UTable
:rows="props.item.createddocuments.filter(i => !i.archived)" :data="props.item.createddocuments.filter(i => !i.archived)"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="selectItem" :on-select="(row) => selectItem(row.original)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
style="height: 70vh" style="height: 70vh"
> >
<template #type-data="{row}"> <template #type-cell="{ row }">
{{dataStore.documentTypesForCreation[row.type].labelSingle}} {{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}
</template> </template>
<template #state-data="{row}"> <template #state-cell="{ row }">
<span <span
v-if="row.state === 'Entwurf'" v-if="row.original.state === 'Entwurf'"
class="text-rose-500" class="text-error-500"
> >
{{row.state}} {{ row.original.state }}
</span> </span>
<span <span
v-if="row.state === 'Gebucht'" v-if="row.original.state === 'Gebucht'"
class="text-cyan-500" class="text-cyan-500"
> >
{{row.state}} {{ row.original.state }}
</span> </span>
<span <span
v-if="row.state === 'Abgeschlossen'" v-if="row.original.state === 'Abgeschlossen'"
class="text-primary-500" class="text-primary-500"
> >
{{row.state}} {{ row.original.state }}
</span> </span>
</template> </template>
<!-- <template #paid-data="{row}"> <!-- <template #paid-data="{row}">
@@ -285,19 +287,19 @@ const selectItem = (item) => {
<span v-else class="text-rose-600">Offen</span> <span v-else class="text-rose-600">Offen</span>
</div> </div>
</template>--> </template>-->
<template #reference-data="{row}"> <template #reference-cell="{ row }">
<span v-if="row === props.item.createddocuments[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span> <span v-if="row.original === props.item.createddocuments[selectedItem]" class="text-primary-500 font-bold">{{ row.original.documentNumber }}</span>
<span v-else>{{row.documentNumber}}</span> <span v-else>{{ row.original.documentNumber }}</span>
</template> </template>
<template #date-data="{row}"> <template #date-cell="{ row }">
<span v-if="row.date">{{row.date ? dayjs(row.date).format("DD.MM.YY") : ''}}</span> <span v-if="row.original.date">{{ row.original.date ? dayjs(row.original.date).format("DD.MM.YY") : '' }}</span>
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).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>
<template #dueDate-data="{row}"> <template #dueDate-cell="{ 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> <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>
<template #amount-data="{row}"> <template #amount-cell="{ row }">
<span v-if="row.type !== 'deliveryNotes'">{{useCurrency(useSum().getCreatedDocumentSum(row, createddocuments))}}</span> <span v-if="row.original.type !== 'deliveryNotes'">{{ useCurrency(useSum().getCreatedDocumentSum(row.original, createddocuments)) }}</span>
</template> </template>
</UTable> </UTable>

View File

@@ -94,41 +94,43 @@ function isImage(file) {
</UCard> </UCard>
<!-- 📱 PDF / IMG Viewer Slideover --> <!-- 📱 PDF / IMG Viewer Slideover -->
<UModal v-model="showViewer" side="bottom" class="h-[100dvh]" fullscreen> <UModal v-model:open="showViewer" side="bottom" class="h-[100dvh]" fullscreen>
<!-- Header --> <template #content>
<div class="p-4 border-b flex justify-between items-center flex-shrink-0"> <!-- Header -->
<h3 class="font-bold truncate max-w-[70vw]">{{ activeFile?.path?.split("/").pop() }}</h3> <div class="p-4 border-b flex justify-between items-center flex-shrink-0">
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="closeViewer" /> <h3 class="font-bold truncate max-w-[70vw]">{{ activeFile?.path?.split("/").pop() }}</h3>
</div> <UButton icon="i-heroicons-x-mark" variant="ghost" @click="closeViewer" />
<!-- 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> </div>
<!-- IMAGE --> <!-- Content -->
<div <div class="flex-1 overflow-y-auto m-2">
v-else-if="activeFile && isImage(activeFile)" <!-- PDF -->
class="p-4 flex justify-center" <div v-if="activeFile && isPdf(activeFile)" class="h-full">
> <PDFViewer
<img :no-controls="true"
:src="activeFile.url" :file-id="activeFile.id"
class="max-w-full max-h-[80vh] rounded-lg shadow" 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> </div>
</template>
<UAlert
v-else
title="Nicht unterstützter Dateityp"
icon="i-heroicons-exclamation-triangle"
/>
</div>
</UModal> </UModal>
</template> </template>

View File

@@ -65,7 +65,7 @@ const renderDatapointValue = (datapoint) => {
</template> </template>
<UAlert <UAlert
v-if="props.item.archived" v-if="props.item.archived"
color="rose" color="error"
variant="outline" variant="outline"
:title="`${dataType.labelSingle} archiviert`" :title="`${dataType.labelSingle} archiviert`"
icon="i-heroicons-archive-box" icon="i-heroicons-archive-box"

View File

@@ -77,21 +77,21 @@ const renderedAllocations = computed(() => {
<UCard class="mt-5"> <UCard class="mt-5">
<UTable <UTable
v-if="props.item.statementallocations" v-if="props.item.statementallocations"
:rows="renderedAllocations" :data="renderedAllocations"
:columns="[{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}]" :columns="normalizeTableColumns([{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}])"
@select="(i) => selectAllocation(i)" :on-select="(i) => selectAllocation(i)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
> >
<template #amount-data="{row}"> <template #amount-cell="{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-rose-600" 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.amount > 0 || row.color === 'green'">{{useCurrency(row.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.amount)}}</span> <span v-else>{{useCurrency(row.original.amount)}}</span>
</template> </template>
<template #date-data="{row}"> <template #date-cell="{row}">
{{row.date ? dayjs(row.date).format('DD.MM.YYYY') : ''}} {{row.original.date ? dayjs(row.original.date).format('DD.MM.YYYY') : ''}}
</template> </template>
<template #description-data="{row}"> <template #description-cell="{row}">
{{row.description ? row.description : ''}} {{row.original.description ? row.original.description : ''}}
</template> </template>
</UTable> </UTable>
</UCard> </UCard>

View File

@@ -95,26 +95,26 @@ const changeActivePhase = async (key) => {
<UAccordion <UAccordion
:items="renderedPhases" :items="renderedPhases"
> >
<template #default="{item,index,open}"> <template #default="slotProps">
<UButton <UButton
variant="ghost" variant="ghost"
:color="item.active ? 'primary' : 'white'" :color="slotProps.item.active ? 'primary' : 'white'"
class="mb-1" class="mb-1"
:disabled="true" :disabled="true"
> >
<template #leading> <template #leading>
<div class="w-6 h-6 flex items-center justify-center -my-1"> <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> </div>
</template> </template>
<span class="truncate"> {{item.label}}</span> <span class="truncate"> {{ slotProps.item.label }}</span>
<template #trailing> <template #trailing>
<UIcon <UIcon
name="i-heroicons-chevron-right-20-solid" name="i-heroicons-chevron-right-20-solid"
class="w-5 h-5 ms-auto transform transition-transform duration-200" class="w-5 h-5 ms-auto transform transition-transform duration-200"
:class="[open && 'rotate-90']" :class="[slotProps?.open && 'rotate-90']"
/> />
</template> </template>

View File

@@ -67,40 +67,40 @@ const columns = [
<UCard class="mt-5"> <UCard class="mt-5">
<UTable <UTable
class="mt-3" class="mt-3"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="props.item.times" :data="props.item.times"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }"
> >
<template #state-data="{row}"> <template #state-cell="{ row }">
<span <span
v-if="row.state === 'Entwurf'" v-if="row.original.state === 'Entwurf'"
class="text-rose-500" class="text-error-500"
>{{row.state}}</span> >{{ row.original.state }}</span>
<span <span
v-if="row.state === 'Eingereicht'" v-if="row.original.state === 'Eingereicht'"
class="text-cyan-500" class="text-cyan-500"
>{{row.state}}</span> >{{ row.original.state }}</span>
<span <span
v-if="row.state === 'Bestätigt'" v-if="row.original.state === 'Bestätigt'"
class="text-primary-500" class="text-primary-500"
>{{row.state}}</span> >{{ row.original.state }}</span>
</template> </template>
<template #user-data="{row}"> <template #user-cell="{ row }">
{{row.profile ? row.profile.fullName : "" }} {{ row.original.profile ? row.original.profile.fullName : "" }}
</template> </template>
<template #startDate-data="{row}"> <template #startDate-cell="{ row }">
{{dayjs(row.startDate).format("DD.MM.YY HH:mm")}} {{ dayjs(row.original.startDate).format("DD.MM.YY HH:mm") }}
</template> </template>
<template #endDate-data="{row}"> <template #endDate-cell="{ row }">
{{dayjs(row.endDate).format("DD.MM.YY HH:mm")}} {{ dayjs(row.original.endDate).format("DD.MM.YY HH:mm") }}
</template> </template>
<template #duration-data="{row}"> <template #duration-cell="{ row }">
{{Math.floor(dayjs(row.endDate).diff(row.startDate, "minutes")/60)}}:{{String(dayjs(row.endDate).diff(row.startDate, "minutes") % 60).padStart(2,"0")}} h {{ 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>
<template #project-data="{row}"> <template #project-cell="{ row }">
{{row.project ? row.project.name : "" }} {{ row.original.project ? row.original.project.name : "" }}
</template> </template>
</UTable> </UTable>
</UCard> </UCard>

View File

@@ -58,76 +58,101 @@
const dataType = dataStore.dataTypes[props.type] const dataType = dataStore.dataTypes[props.type]
const selectedItem = ref(0) const selectedItem = ref(0)
const sort = ref({ const sorting = ref([{
column: dataType.sortColumn || "date", id: dataType.sortColumn || "date",
direction: 'desc' 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> </script>
<template> <template>
<UTable <UTable
:loading="props.loading" :loading="props.loading"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
sort-mode="manual" sort-mode="manual"
v-model:sort="sort" v-model:sorting="sorting"
@update:sort="emit('sort',{sort_column: sort.column, sort_direction: sort.direction})" @update:sorting="handleSortChange"
v-if="dataType && columns" v-if="dataType && columns"
:rows="props.rows" :data="props.rows"
:columns="props.columns" :columns="normalizedColumns"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(getShowRoute(type, i.id))" :on-select="handleSelect"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }" :empty="`Keine ${dataType.label} anzuzeigen`"
> >
<!-- <template <template #name-cell="{ row }">
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}">
<span <span
v-if="row.id === props.rows[selectedItem].id" v-if="row.original.id === props.rows[selectedItem]?.id"
class="text-primary-500 font-bold"> class="block truncate text-primary-500 font-bold"
<UTooltip >
:text="row.name" <UTooltip :text="row.original.name">
> <span class="block truncate">
{{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}} {{ truncateValue(row.original.name, dataType.templateColumns.find(i => i.key === "name")?.maxLength) }}
</span>
</UTooltip> </span> </UTooltip> </span>
<span v-else> <span v-else class="block truncate">
<UTooltip <UTooltip :text="row.original.name">
:text="row.name" <span class="block truncate">
> {{ truncateValue(row.original.name, dataType.templateColumns.find(i => i.key === "name")?.maxLength) }}
{{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>
</UTooltip> </UTooltip>
</span> </span>
</template> </template>
<template #fullName-data="{row}"> <template #fullName-cell="{ row }">
<span <span
v-if="row.id === props.rows[selectedItem].id" v-if="row.original.id === props.rows[selectedItem]?.id"
class="text-primary-500 font-bold">{{row.fullName}} class="block truncate text-primary-500 font-bold">{{ row.original.fullName }}
</span> </span>
<span v-else> <span v-else class="block truncate">
{{row.fullName}} {{ row.original.fullName }}
</span> </span>
</template> </template>
<template #licensePlate-data="{row}"> <template #licensePlate-cell="{ row }">
<span <span
v-if="row.id === props.rows[selectedItem].id" v-if="row.original.id === props.rows[selectedItem]?.id"
class="text-primary-500 font-bold">{{row.licensePlate}} class="block truncate text-primary-500 font-bold">{{ row.original.licensePlate }}
</span> </span>
<span v-else> <span v-else class="block truncate">
{{row.licensePlate}} {{ row.original.licensePlate }}
</span> </span>
</template> </template>
<template <template
v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)" v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)"
v-slot:[`${column.key}-data`]="{row}"> v-slot:[`${column.key}-cell`]="{ row }">
<component v-if="column.component" :is="column.component" :row="row"></component> <component v-if="column.component" :is="column.component" :row="row.original"></component>
<span v-else-if="row[column.key]"> <span v-else-if="row.original[column.key]" class="block truncate">
<UTooltip :text="row[column.key]"> <UTooltip :text="String(row.original[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 : ''}`: ''}} <span class="block truncate">
</UTooltip> {{ `${truncateValue(row.original[column.key], column.maxLength)}${column.unit ? ` ${column.unit}` : ''}` }}
</span>
</UTooltip>
</span> </span>
</template> </template>

View File

@@ -77,7 +77,7 @@
<!-- <UTable <!-- <UTable
v-if="dataType && columns" v-if="dataType && columns"
:rows="props.rows" :data="props.rows"
:columns="props.columns" :columns="props.columns"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"

View File

@@ -55,17 +55,19 @@ setup()
</script> </script>
<template> <template>
<UModal v-model="showMessageModal" prevent-close> <UModal v-model:open="showMessageModal" prevent-close>
<UCard> <template #content>
<template #header> <UCard>
<span class="font-bold">{{messageToShow.title}}</span> <template #header>
</template> <span class="font-bold">{{messageToShow.title}}</span>
<p class=" my-2" v-html="messageToShow.description"></p> </template>
<UButton <p class=" my-2" v-html="messageToShow.description"></p>
variant="outline" <UButton
@click="markMessageAsRead" variant="outline"
>Gelesen</UButton> @click="markMessageAsRead"
</UCard> >Gelesen</UButton>
</UCard>
</template>
</UModal> </UModal>
<!-- <UCard <!-- <UCard
@@ -79,7 +81,7 @@ setup()
variant="ghost" variant="ghost"
@click="showMessage(globalMessages[0])" @click="showMessage(globalMessages[0])"
/> />
<UModal v-model="showMessageModal"> <UModal v-model:open="showMessageModal">
<UCard> <UCard>
<template #header> <template #header>
<span class="font-bold">{{messageToShow.title}}</span> <span class="font-bold">{{messageToShow.title}}</span>

View File

@@ -123,18 +123,20 @@ function onSelect (option) {
/> />
<UModal <UModal
v-model="showCommandPalette" v-model:open="showCommandPalette"
> >
<UCommandPalette <template #content>
v-model="selectedCommand" <UCommandPalette
:groups="groups" v-model="selectedCommand"
:autoselect="false" :groups="groups"
@update:model-value="onSelect" :autoselect="false"
ref="commandPaletteRef" @update:model-value="onSelect"
/> ref="commandPaletteRef"
/>
</template>
</UModal> </UModal>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@@ -1,6 +1,16 @@
<script setup> <script setup>
import dayjs from 'dayjs'
const { isHelpSlideoverOpen } = useDashboard() 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 shortcuts = ref(false)
const query = ref('') const query = ref('')
@@ -122,7 +132,7 @@ const addContactRequest = async () => {
toast.add({title: "Anfrage erfolgreich erstellt"}) toast.add({title: "Anfrage erfolgreich erstellt"})
resetContactRequest() resetContactRequest()
} else { } else {
toast.add({title: "Anfrage konnte nicht erstellt werden",color:"rose"}) toast.add({title: "Anfrage konnte nicht erstellt werden",color:"error"})
} }
loadingContactRequest.value = false loadingContactRequest.value = false
} }
@@ -133,10 +143,25 @@ const resetContactRequest = () => {
title: "", title: "",
} }
} }
const lastOpenedLabel = computed(() => {
if (!seenState.value.lastOpenedAt) return 'Noch nicht geöffnet'
return dayjs(seenState.value.lastOpenedAt).format('DD.MM.YYYY HH:mm')
})
const changelogEntries = computed(() => entries.value.slice(0, 12))
watch(isHelpSlideoverOpen, async (isOpen) => {
if (!isOpen || shortcuts.value) return
await refresh(true)
markAsSeen()
})
</script> </script>
<template> <template>
<UDashboardSlideover v-model="isHelpSlideoverOpen"> <USlideover v-model:open="isHelpSlideoverOpen" side="right">
<template #title> <template #title>
<UButton <UButton
v-if="shortcuts" v-if="shortcuts"
@@ -150,30 +175,94 @@ const resetContactRequest = () => {
{{ shortcuts ? 'Shortcuts' : 'Hilfe & Information' }} {{ shortcuts ? 'Shortcuts' : 'Hilfe & Information' }}
</template> </template>
<div v-if="shortcuts" class="space-y-6"> <template #body>
<UInput v-model="query" icon="i-heroicons-magnifying-glass" placeholder="Search..." autofocus color="gray" /> <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"> <div v-for="(category, index) in filteredCategories" :key="index">
<p class="mb-3 text-sm text-gray-900 dark:text-white font-semibold"> <p class="mb-3 text-sm text-gray-900 dark:text-white font-semibold">
{{ category.title }} {{ category.title }}
</p> </p>
<div class="space-y-2"> <div class="space-y-2">
<div v-for="(item, i) in category.items" :key="i" class="flex items-center justify-between"> <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> <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"> <div class="flex items-center justify-end flex-shrink-0 gap-0.5">
<UKbd v-for="(shortcut, j) in item.shortcuts" :key="j"> <UKbd v-for="(shortcut, j) in item.shortcuts" :key="j">
{{ shortcut }} {{ shortcut }}
</UKbd> </UKbd>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div v-else class="flex flex-col gap-y-6">
<div v-else class="flex flex-col gap-y-3"> <div class="flex flex-col gap-y-3">
<UButton v-for="(link, index) in links" :key="index" color="white" v-bind="link" /> <UButton v-for="(link, index) in links" :key="index" color="white" v-bind="link" />
</div> </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"> <!-- <div class="mt-5" v-if="!loadingContactRequest">
<h1 class="font-semibold">Kontaktanfrage:</h1> <h1 class="font-semibold">Kontaktanfrage:</h1>
<UForm <UForm
@@ -181,29 +270,29 @@ const resetContactRequest = () => {
@submit="addContactRequest" @submit="addContactRequest"
@reset="resetContactRequest" @reset="resetContactRequest"
> >
&lt;!&ndash; <UFormGroup &lt;!&ndash; <UFormField
label="Art:" label="Art:"
> >
<USelectMenu <USelectMenu
:options="['Hilfe','Software Problem / Bug','Funktionsanfrage','Kontakt','Sonstiges']" :options="['Hilfe','Software Problem / Bug','Funktionsanfrage','Kontakt','Sonstiges']"
v-model="contactRequestData.contactType" v-model="contactRequestData.contactType"
/> />
</UFormGroup>&ndash;&gt; </UFormField>&ndash;&gt;
<UFormGroup <UFormField
label="Titel:" label="Titel:"
> >
<UInput <UInput
v-model="contactRequestData.title" v-model="contactRequestData.title"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Nachricht:" label="Nachricht:"
> >
<UTextarea <UTextarea
v-model="contactRequestData.message" v-model="contactRequestData.message"
rows="6" rows="6"
/> />
</UFormGroup> </UFormField>
<InputGroup class="mt-3"> <InputGroup class="mt-3">
<UButton <UButton
type="submit" type="submit"
@@ -213,7 +302,7 @@ const resetContactRequest = () => {
</UButton> </UButton>
<UButton <UButton
type="reset" type="reset"
color="rose" color="error"
variant="outline" variant="outline"
:disabled="!contactRequestData.title && !contactRequestData.message" :disabled="!contactRequestData.title && !contactRequestData.message"
> >
@@ -224,5 +313,6 @@ const resetContactRequest = () => {
</UForm> </UForm>
</div> </div>
<UProgress class="mt-5" animation="carousel" v-else/>--> <UProgress class="mt-5" animation="carousel" v-else/>-->
</UDashboardSlideover> </template>
</USlideover>
</template> </template>

View File

@@ -76,38 +76,40 @@ const renderText = (text) => {
<template> <template>
<UModal <UModal
v-model="showAddHistoryItemModal" v-model:open="showAddHistoryItemModal"
> >
<UCard class="h-full"> <template #content>
<template #header> <UCard class="h-full">
<div class="flex items-center justify-between"> <template #header>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <div class="flex items-center justify-between">
Eintrag hinzufügen <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
</h3> Eintrag hinzufügen
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showAddHistoryItemModal = false" /> </h3>
</div> <UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showAddHistoryItemModal = false" />
</template> </div>
</template>
<UFormGroup <UFormField
label="Text:" label="Text:"
> >
<UTextarea <UTextarea
v-model="addHistoryItemData.text" v-model="addHistoryItemData.text"
@keyup.meta.enter="addHistoryItem" @keyup.meta.enter="addHistoryItem"
/> />
<!-- TODO: Add Dropdown and Checking for Usernames --> <!-- TODO: Add Dropdown and Checking for Usernames -->
<!-- <template #help> <!-- <template #help>
<UKbd>{{metaSymbol}}</UKbd> <UKbd>Enter</UKbd> Speichern <UKbd>{{metaSymbol}}</UKbd> <UKbd>Enter</UKbd> Speichern
</template>--> </template>-->
</UFormGroup> </UFormField>
<template #footer> <template #footer>
<UButton @click="addHistoryItem">Speichern</UButton> <UButton @click="addHistoryItem">Speichern</UButton>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
<Toolbar <Toolbar
v-if="!props.renderHeadline && props.elementId && props.type" v-if="!props.renderHeadline && props.elementId && props.type"
@@ -127,7 +129,7 @@ const renderText = (text) => {
+ Eintrag + Eintrag
</UButton> </UButton>
</div> </div>
<UDivider class="my-3"/> <USeparator class="my-3"/>
</div> </div>
<!-- ITEM LIST --> <!-- ITEM LIST -->
@@ -136,7 +138,7 @@ const renderText = (text) => {
v-if="items.length > 0" v-if="items.length > 0"
v-for="(item,index) in items.slice().reverse()" v-for="(item,index) in items.slice().reverse()"
> >
<UDivider <USeparator
class="my-3" class="my-3"
v-if="index !== 0" v-if="index !== 0"
/> />

View File

@@ -86,7 +86,7 @@ defineShortcuts({
</p> </p>
</div> </div>
<UDivider /> <USeparator />
</div> </div>
</UDashboardPanelContent> </UDashboardPanelContent>
</template> </template>

View File

@@ -34,7 +34,7 @@ defineProps({
</p> </p>
</div> </div>
<UDivider class="my-5" /> <USeparator class="my-5" />
<div class="flex-1"> <div class="flex-1">
<p class="text-lg"> <p class="text-lg">
@@ -42,7 +42,7 @@ defineProps({
</p> </p>
</div> </div>
<UDivider class="my-5" /> <USeparator class="my-5" />
<form @submit.prevent> <form @submit.prevent>
<UTextarea color="gray" required size="xl" :rows="5" :placeholder="`Reply to ${mail.from.name}`"> <UTextarea color="gray" required size="xl" :rows="5" :placeholder="`Reply to ${mail.from.name}`">

View File

@@ -90,7 +90,8 @@ watch(() => labelPrinter.connected, (connected) => {
<template> <template>
<UModal :ui="{ width: 'sm:max-w-5xl' }"> <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> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -133,6 +134,7 @@ watch(() => labelPrinter.connected, (connected) => {
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -18,21 +18,23 @@ const handleClick = async () => {
<template> <template>
<!-- Printer Button --> <!-- Printer Button -->
<UModal v-model="showPrinterInfo"> <UModal v-model:open="showPrinterInfo">
<UCard> <template #content>
<template #header> <UCard>
<div class="flex items-center justify-between"> <template #header>
<h3 class="text-lg font-semibold">Drucker Informationen</h3> <div class="flex items-center justify-between">
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="showPrinterInfo = false" /> <h3 class="text-lg font-semibold">Drucker Informationen</h3>
</div> <UButton icon="i-heroicons-x-mark" variant="ghost" @click="showPrinterInfo = false" />
</template> </div>
<p>Seriennummer: {{labelPrinter.info.serial}}</p> </template>
<p>MAC: {{labelPrinter.info.mac}}</p> <p>Seriennummer: {{labelPrinter.info.serial}}</p>
<p>Modell: {{labelPrinter.info.modelId}}</p> <p>MAC: {{labelPrinter.info.mac}}</p>
<p>Charge: {{labelPrinter.info.charge}}</p> <p>Modell: {{labelPrinter.info.modelId}}</p>
<p>Hardware Version: {{labelPrinter.info.hardwareVersion}}</p> <p>Charge: {{labelPrinter.info.charge}}</p>
<p>Software Version: {{labelPrinter.info.softwareVersion}}</p> <p>Hardware Version: {{labelPrinter.info.hardwareVersion}}</p>
</UCard> <p>Software Version: {{labelPrinter.info.softwareVersion}}</p>
</UCard>
</template>
</UModal> </UModal>
<UButton <UButton
@@ -50,4 +52,4 @@ const handleClick = async () => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -1,10 +1,14 @@
<script setup> <script setup>
const props = defineProps({
collapsed: {
type: Boolean,
default: false
}
})
const route = useRoute() const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const { has } = usePermission() const { has } = usePermission()
// Lokaler State für den Taschenrechner
const showCalculator = ref(false)
const tenantExtraModules = computed(() => { const tenantExtraModules = computed(() => {
const modules = auth.activeTenantData?.extraModules const modules = auth.activeTenantData?.extraModules
return Array.isArray(modules) ? modules : [] return Array.isArray(modules) ? modules : []
@@ -18,6 +22,18 @@ const showMemberRelationsNav = computed(() => {
const isAdmin = computed(() => Boolean(auth.user?.is_admin)) const isAdmin = computed(() => Boolean(auth.user?.is_admin))
const tenantFeatures = computed(() => auth.activeTenantData?.features || {}) const tenantFeatures = computed(() => auth.activeTenantData?.features || {})
const featureEnabled = (key) => tenantFeatures.value?.[key] !== false 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 links = computed(() => {
const organisationChildren = [ const organisationChildren = [
@@ -36,7 +52,7 @@ const links = computed(() => {
to: "/wiki", to: "/wiki",
icon: "i-heroicons-book-open" icon: "i-heroicons-book-open"
} : null, } : null,
].filter(Boolean) ]
const documentChildren = [ const documentChildren = [
featureEnabled("files") ? { featureEnabled("files") ? {
@@ -56,7 +72,7 @@ const links = computed(() => {
icon: "i-heroicons-archive-box", icon: "i-heroicons-archive-box",
disabled: true disabled: true
} : null, } : null,
].filter(Boolean) ]
const communicationChildren = [ const communicationChildren = [
featureEnabled("helpdesk") ? { featureEnabled("helpdesk") ? {
@@ -71,7 +87,7 @@ const links = computed(() => {
icon: "i-heroicons-envelope", icon: "i-heroicons-envelope",
disabled: true disabled: true
} : null, } : null,
].filter(Boolean) ]
const contactsChildren = [ const contactsChildren = [
showMembersNav.value && featureEnabled("members") ? { showMembersNav.value && featureEnabled("members") ? {
@@ -94,7 +110,7 @@ const links = computed(() => {
to: "/standardEntity/contacts", to: "/standardEntity/contacts",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
} : null, } : null,
].filter(Boolean) ]
const staffChildren = [ const staffChildren = [
featureEnabled("staffTime") ? { featureEnabled("staffTime") ? {
@@ -102,7 +118,7 @@ const links = computed(() => {
to: "/staff/time", to: "/staff/time",
icon: "i-heroicons-clock", icon: "i-heroicons-clock",
} : null, } : null,
].filter(Boolean) ]
const accountingChildren = [ const accountingChildren = [
featureEnabled("createDocument") ? { featureEnabled("createDocument") ? {
@@ -120,6 +136,11 @@ const links = computed(() => {
to: "/incomingInvoices", to: "/incomingInvoices",
icon: "i-heroicons-document-text", icon: "i-heroicons-document-text",
} : null, } : null,
(featureEnabled("createDocument") || featureEnabled("incomingInvoices")) ? {
label: "USt-Auswertung",
to: "/accounting/tax",
icon: "i-heroicons-calculator",
} : null,
featureEnabled("costcentres") ? { featureEnabled("costcentres") ? {
label: "Kostenstellen", label: "Kostenstellen",
to: "/standardEntity/costcentres", to: "/standardEntity/costcentres",
@@ -140,7 +161,7 @@ const links = computed(() => {
to: "/banking", to: "/banking",
icon: "i-heroicons-document-text", icon: "i-heroicons-document-text",
} : null, } : null,
].filter(Boolean) ]
const inventoryChildren = [ const inventoryChildren = [
has("spaces") && featureEnabled("spaces") ? { has("spaces") && featureEnabled("spaces") ? {
@@ -168,7 +189,7 @@ const links = computed(() => {
to: "/standardEntity/inventoryitemgroups", to: "/standardEntity/inventoryitemgroups",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
} : null, } : null,
].filter(Boolean) ]
const masterDataChildren = [ const masterDataChildren = [
has("products") && featureEnabled("products") ? { has("products") && featureEnabled("products") ? {
@@ -221,7 +242,7 @@ const links = computed(() => {
to: "/standardEntity/vehicles", to: "/standardEntity/vehicles",
icon: "i-heroicons-truck" icon: "i-heroicons-truck"
} : null, } : null,
].filter(Boolean) ]
const settingsChildren = [ const settingsChildren = [
featureEnabled("settingsNumberRanges") ? { featureEnabled("settingsNumberRanges") ? {
@@ -259,24 +280,32 @@ const links = computed(() => {
to: "/export", to: "/export",
icon: "i-heroicons-clipboard-document-list" icon: "i-heroicons-clipboard-document-list"
} : null, } : null,
].filter(Boolean) ]
return [ const visibleOrganisationChildren = visibleItems(organisationChildren)
const visibleDocumentChildren = visibleItems(documentChildren)
const visibleCommunicationChildren = visibleItems(communicationChildren)
const visibleContactsChildren = visibleItems(contactsChildren)
const visibleStaffChildren = visibleItems(staffChildren)
const visibleAccountingChildren = visibleItems(accountingChildren)
const visibleInventoryChildren = visibleItems(inventoryChildren)
const visibleMasterDataChildren = visibleItems(masterDataChildren)
const visibleSettingsChildren = visibleItems(settingsChildren)
return visibleItems([
...(auth.profile?.pinned_on_navigation || []).map(pin => { ...(auth.profile?.pinned_on_navigation || []).map(pin => {
if (pin.type === "external") { if (pin.type === "external") {
return { return {
label: pin.label, label: pin.label,
to: pin.link, to: pin.link,
icon: pin.icon, icon: pin.icon,
target: "_blank", target: "_blank"
pinned: true
} }
} else if (pin.type === "standardEntity") { } else if (pin.type === "standardEntity") {
return { return {
label: pin.label, label: pin.label,
to: pin.datatype === "tasks" ? `/tasks/show/${pin.id}` : `/standardEntity/${pin.datatype}/show/${pin.id}`, to: pin.datatype === "tasks" ? `/tasks/show/${pin.id}` : `/standardEntity/${pin.datatype}/show/${pin.id}`,
icon: pin.icon, icon: pin.icon
pinned: true
} }
} }
}), }),
@@ -293,55 +322,6 @@ const links = computed(() => {
to: "/historyitems", to: "/historyitems",
icon: "i-heroicons-book-open" icon: "i-heroicons-book-open"
} : null, } : null,
...(organisationChildren.length > 0 ? [{
label: "Organisation",
icon: "i-heroicons-rectangle-stack",
defaultOpen: false,
children: organisationChildren
}] : []),
...(documentChildren.length > 0 ? [{
label: "Dokumente",
icon: "i-heroicons-rectangle-stack",
defaultOpen: false,
children: documentChildren
}] : []),
...(communicationChildren.length > 0 ? [{
label: "Kommunikation",
icon: "i-heroicons-megaphone",
defaultOpen: false,
children: communicationChildren
}] : []),
...(contactsChildren.length > 0 ? [{
label: "Kontakte",
defaultOpen: false,
icon: "i-heroicons-user-group",
children: contactsChildren
}] : []),
...(staffChildren.length > 0 ? [{
label: "Mitarbeiter",
defaultOpen: false,
icon: "i-heroicons-user-group",
children: staffChildren
}] : []),
...(accountingChildren.length > 0 ? [{
label: "Buchhaltung",
defaultOpen: false,
icon: "i-heroicons-chart-bar-square",
children: accountingChildren
}] : []),
...(inventoryChildren.length > 0 ? [{
label: "Lager",
icon: "i-heroicons-puzzle-piece",
defaultOpen: false,
children: inventoryChildren
}] : []),
...(masterDataChildren.length > 0 ? [{
label: "Stammdaten",
defaultOpen: false,
icon: "i-heroicons-clipboard-document",
children: masterDataChildren
}] : []),
...(has("projects") && featureEnabled("projects")) ? [{ ...(has("projects") && featureEnabled("projects")) ? [{
label: "Projekte", label: "Projekte",
to: "/standardEntity/projects", to: "/standardEntity/projects",
@@ -357,90 +337,139 @@ const links = computed(() => {
to: "/standardEntity/plants", to: "/standardEntity/plants",
icon: "i-heroicons-clipboard-document" icon: "i-heroicons-clipboard-document"
}] : [], }] : [],
...(settingsChildren.length > 0 ? [{ ...(visibleOrganisationChildren.length > 0 ? [{
label: "Organisation",
icon: "i-heroicons-rectangle-stack",
defaultOpen: false,
children: visibleOrganisationChildren
}] : []),
...(visibleDocumentChildren.length > 0 ? [{
label: "Dokumente",
icon: "i-heroicons-rectangle-stack",
defaultOpen: false,
children: visibleDocumentChildren
}] : []),
...(visibleCommunicationChildren.length > 0 ? [{
label: "Kommunikation",
icon: "i-heroicons-megaphone",
defaultOpen: false,
children: visibleCommunicationChildren
}] : []),
...(visibleContactsChildren.length > 0 ? [{
label: "Kontakte",
defaultOpen: false,
icon: "i-heroicons-user-group",
children: visibleContactsChildren
}] : []),
...(visibleStaffChildren.length > 0 ? [{
label: "Mitarbeiter",
defaultOpen: false,
icon: "i-heroicons-user-group",
children: visibleStaffChildren
}] : []),
...(visibleAccountingChildren.length > 0 ? [{
label: "Buchhaltung",
defaultOpen: false,
icon: "i-heroicons-chart-bar-square",
children: visibleAccountingChildren
}] : []),
...(visibleInventoryChildren.length > 0 ? [{
label: "Lager",
icon: "i-heroicons-puzzle-piece",
defaultOpen: false,
children: visibleInventoryChildren
}] : []),
...(visibleMasterDataChildren.length > 0 ? [{
label: "Stammdaten",
defaultOpen: false,
icon: "i-heroicons-clipboard-document",
children: visibleMasterDataChildren
}] : []),
...(visibleSettingsChildren.length > 0 ? [{
label: "Einstellungen", label: "Einstellungen",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-cog-8-tooth", icon: "i-heroicons-cog-8-tooth",
children: settingsChildren children: visibleSettingsChildren
}] : []), }] : []),
].filter(Boolean) ])
}) })
const accordionItems = computed(() => const navItems = computed(() =>
links.value.filter(item => Array.isArray(item.children) && item.children.length > 0) links.value
) .filter(Boolean)
.map((item, index) => {
const children = Array.isArray(item.children)
? item.children.map((child, childIndex) => ({
...child,
value: child.id || child.label || `${index}-${childIndex}`,
active: isRouteActive(child.to)
}))
: undefined
const buttonItems = computed(() => const active = item.active || isRouteActive(item.to) || Boolean(children?.some(child => child.active))
links.value.filter(item => !item.children || item.children.length === 0)
return {
...item,
children,
value: item.id || item.label || String(index),
defaultOpen: item.defaultOpen || active,
active,
tooltip: true,
popover: true,
trailingIcon: children?.length ? undefined : ''
}
})
) )
</script> </script>
<template> <template>
<div class="flex flex-col gap-1"> <UNavigationMenu
<UButton :items="navItems"
v-for="item in buttonItems" orientation="vertical"
:key="item.label" :collapsed="props.collapsed"
variant="ghost" tooltip
:color="(item.to && route.path === item.to) ? 'primary' : (item.pinned ? 'amber' : 'gray')" popover
:icon="item.pinned ? 'i-heroicons-star' : item.icon" color="neutral"
class="w-full" highlight
:to="item.to" highlight-color="primary"
:target="item.target" class="w-full"
@click="item.click ? item.click() : null" :ui="{
> root: 'w-full',
<UIcon list: 'space-y-1',
v-if="item.pinned" link: 'min-w-0 rounded-lg px-2.5 py-2',
:name="item.icon" linkLeadingIcon: 'size-5 shrink-0',
class="w-5 h-5 me-2" linkLabel: 'truncate',
/> childList: 'ms-0 space-y-1 border-l border-default ps-3',
{{ item.label }} childLink: 'min-w-0 rounded-lg px-2 py-1.5',
</UButton> childLinkLabel: 'truncate'
</div> }"
<UDivider class="my-2"/>
<UAccordion
:items="accordionItems"
:multiple="false"
class="mt-2"
> >
<template #default="{ item, open }"> <template #item-leading="{ item, active }">
<UButton <UIcon
variant="ghost" v-if="item.icon"
:color="(item.children?.some(c => route.path.includes(c.to))) ? 'primary' : 'gray'" :name="item.icon"
:icon="item.icon" class="size-5 shrink-0"
class="w-full" :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 }} {{ item.badge }}
<template #trailing> </UBadge>
<UIcon <UIcon
name="i-heroicons-chevron-right-20-solid" v-else-if="item.children?.length"
class="w-5 h-5 ms-auto transform transition-transform duration-200" name="i-heroicons-chevron-down-20-solid"
:class="[open && 'rotate-90']" class="size-4 shrink-0 transition-transform"
/> :class="active ? 'text-primary' : 'text-muted'"
</template> />
</UButton>
</template> </template>
</UNavigationMenu>
<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"/>
</template> </template>

View File

@@ -36,28 +36,30 @@ const setNotificationAsRead = async (notification) => {
</script> </script>
<template> <template>
<UDashboardSlideover v-model="isNotificationsSlideoverOpen" title="Benachrichtigungen"> <USlideover v-model:open="isNotificationsSlideoverOpen" title="Benachrichtigungen" side="right">
<NuxtLink <template #body>
v-for="notification in notifications" <NuxtLink
:key="notification.id" v-for="notification in notifications"
:to="notification.link" :key="notification.id"
class="p-3 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer flex items-center gap-3 relative" :to="notification.link"
@click="setNotificationAsRead(notification)" 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 color="primary" :show="!notification.read && !notification.readAt" inset>
</UChip> <UAvatar alt="FEDEO" size="md" />
</UChip>
<div class="text-sm flex-1"> <div class="text-sm flex-1">
<p class="flex items-center justify-between"> <p class="flex items-center justify-between">
<span class="text-gray-900 dark:text-white font-medium">{{notification.title}}</span> <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))" /> <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>
<p class="text-gray-500 dark:text-gray-400"> <p class="text-gray-500 dark:text-gray-400">
{{ notification.message }} {{ notification.message }}
</p> </p>
</div> </div>
</NuxtLink> </NuxtLink>
</UDashboardSlideover> </template>
</USlideover>
</template> </template>

View File

@@ -119,7 +119,7 @@ const setDeliveryDateToToday = () => {
<div class="space-y-5"> <div class="space-y-5">
<UFormGroup <UFormField
label="Datum der Ausführung" label="Datum der Ausführung"
:error="errors.deliveryDate" :error="errors.deliveryDate"
required required
@@ -134,9 +134,9 @@ const setDeliveryDateToToday = () => {
/> />
<UButton color="gray" variant="soft" size="lg" label="Heute" @click="setDeliveryDateToToday" /> <UButton color="gray" variant="soft" size="lg" label="Heute" @click="setDeliveryDateToToday" />
</div> </div>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
v-if="!context?.meta?.defaultProfileId && data?.profiles?.length > 0" v-if="!context?.meta?.defaultProfileId && data?.profiles?.length > 0"
label="Mitarbeiter" label="Mitarbeiter"
:error="errors.profile" :error="errors.profile"
@@ -144,16 +144,16 @@ const setDeliveryDateToToday = () => {
> >
<USelectMenu <USelectMenu
v-model="form.profile" v-model="form.profile"
:options="data.profiles" :items="data.profiles"
option-attribute="fullName" label-key="fullName"
value-attribute="id" value-key="id"
placeholder="Name auswählen..." placeholder="Name auswählen..."
searchable searchable
size="lg" size="lg"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
v-if="data?.projects?.length > 0" v-if="data?.projects?.length > 0"
:label="config.ui?.labels?.project || 'Projekt / Auftrag'" :label="config.ui?.labels?.project || 'Projekt / Auftrag'"
:error="errors.project" :error="errors.project"
@@ -161,16 +161,16 @@ const setDeliveryDateToToday = () => {
> >
<USelectMenu <USelectMenu
v-model="form.project" v-model="form.project"
:options="data.projects" :items="data.projects"
option-attribute="name" label-key="name"
value-attribute="id" value-key="id"
placeholder="Wählen..." placeholder="Wählen..."
searchable searchable
size="lg" size="lg"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
v-if="data?.services?.length > 0" v-if="data?.services?.length > 0"
:label="config?.ui?.labels?.service || 'Tätigkeit'" :label="config?.ui?.labels?.service || 'Tätigkeit'"
:error="errors.service" :error="errors.service"
@@ -178,16 +178,16 @@ const setDeliveryDateToToday = () => {
> >
<USelectMenu <USelectMenu
v-model="form.service" v-model="form.service"
:options="data.services" :items="data.services"
option-attribute="name" label-key="name"
value-attribute="id" value-key="id"
placeholder="Wählen..." placeholder="Wählen..."
searchable searchable
size="lg" size="lg"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Menge / Dauer" label="Menge / Dauer"
:error="errors.quantity" :error="errors.quantity"
required required
@@ -203,9 +203,9 @@ const setDeliveryDateToToday = () => {
<span class="text-gray-500 text-sm pr-2">{{ currentUnit }}</span> <span class="text-gray-500 text-sm pr-2">{{ currentUnit }}</span>
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
v-if="config?.features?.agriculture?.showDieselUsage" v-if="config?.features?.agriculture?.showDieselUsage"
label="Dieselverbrauch" label="Dieselverbrauch"
:error="errors.diesel" :error="errors.diesel"
@@ -216,11 +216,11 @@ const setDeliveryDateToToday = () => {
<span class="text-gray-500 text-xs">Liter</span> <span class="text-gray-500 text-xs">Liter</span>
</template> </template>
</UInput> </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..." /> <UTextarea v-model="form.description" :rows="3" placeholder="Optional..." />
</UFormGroup> </UFormField>
</div> </div>

View File

@@ -16,28 +16,30 @@ const onLogout = async () => {
</script> </script>
<template> <template>
<UModal v-model="auth.sessionWarningVisible" prevent-close> <UModal v-model:open="auth.sessionWarningVisible" prevent-close>
<UCard> <template #content>
<template #header> <UCard>
<h3 class="text-lg font-semibold">Sitzung läuft bald ab</h3> <template #header>
</template> <h3 class="text-lg font-semibold">Sitzung läuft bald ab</h3>
</template>
<p class="text-sm text-gray-600 dark:text-gray-300"> <p class="text-sm text-gray-600 dark:text-gray-300">
Deine Sitzung endet in Deine Sitzung endet in
<span class="font-semibold">{{ remainingTimeLabel }}</span>. <span class="font-semibold">{{ remainingTimeLabel }}</span>.
Bitte bestätige, um eingeloggt zu bleiben. Bitte bestätige, um eingeloggt zu bleiben.
</p> </p>
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<UButton variant="outline" color="gray" @click="onLogout"> <UButton variant="outline" color="gray" @click="onLogout">
Abmelden Abmelden
</UButton> </UButton>
<UButton color="primary" @click="onRefresh"> <UButton color="primary" @click="onRefresh">
Eingeloggt bleiben Eingeloggt bleiben
</UButton> </UButton>
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -116,7 +116,7 @@ async function onSubmit(event: FormSubmitEvent<any>) {
emit('saved') emit('saved')
isOpen.value = false isOpen.value = false
} catch (error: any) { } catch (error: any) {
toast.add({ title: 'Fehler', description: error.message, color: 'red' }) toast.add({ title: 'Fehler', description: error.message, color: 'error' })
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -124,57 +124,63 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</script> </script>
<template> <template>
<UModal v-model="isOpen"> <UModal v-model:open="isOpen">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <template #content>
<template #header> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<div class="flex items-center justify-between"> <template #header>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <div class="flex items-center justify-between">
{{ entry ? 'Eintrag bearbeiten' : 'Neue Zeit erfassen' }} <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
</h3> {{ entry ? 'Eintrag bearbeiten' : 'Neue Zeit erfassen' }}
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" /> </h3>
</div> <UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</template> </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"> <div class="grid grid-cols-2 gap-4">
<USelectMenu v-model="state.type" :options="types" value-attribute="value" option-attribute="label" /> <UFormField label="Start Datum" name="start_date">
</UFormGroup> <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"> <div class="grid grid-cols-2 gap-4">
<UFormGroup label="Start Datum" name="start_date"> <UFormField label="Ende Datum" name="end_date">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UInput type="date" v-model="state.start_date" class="flex-1" /> <UInput type="date" v-model="state.end_date" class="flex-1" />
<UButton color="gray" variant="soft" label="Heute" @click="setDateFieldToToday('start_date')" /> <UButton color="gray" variant="soft" label="Heute" @click="setDateFieldToToday('end_date')" />
</div> </div>
</UFormGroup> </UFormField>
<UFormGroup label="Start Zeit" name="start_time"> <UFormField label="Ende Zeit" name="end_time">
<UInput type="time" v-model="state.start_time" /> <UInput type="time" v-model="state.end_time" />
</UFormGroup> </UFormField>
</div> </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"> <UFormField label="Beschreibung / Notiz" name="description">
<UFormGroup label="Ende Datum" name="end_date"> <UTextarea v-model="state.description" placeholder="Was wurde gemacht?" />
<div class="flex items-center gap-2"> </UFormField>
<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>
<UFormGroup label="Beschreibung / Notiz" name="description"> <div class="flex justify-end gap-2 pt-4">
<UTextarea v-model="state.description" placeholder="Was wurde gemacht?" /> <UButton label="Abbrechen" color="gray" variant="ghost" @click="isOpen = false" />
</UFormGroup> <UButton type="submit" label="Speichern" color="primary" :loading="loading" />
</div>
<div class="flex justify-end gap-2 pt-4"> </UForm>
<UButton label="Abbrechen" color="gray" variant="ghost" @click="isOpen = false" /> </UCard>
<UButton type="submit" label="Speichern" color="primary" :loading="loading" /> </template>
</div>
</UForm>
</UCard>
</UModal> </UModal>
</template> </template>

View File

@@ -61,33 +61,35 @@ setupPage()
<template> <template>
<UModal :fullscreen="props.mode === 'show'"> <UModal :fullscreen="props.mode === 'show'">
<EntityShow <template #content>
v-if="loaded && props.mode === 'show'" <EntityShow
:type="props.type" v-if="loaded && props.mode === 'show'"
:item="item" :type="props.type"
@updateNeeded="setupPage" :item="item"
:key="item" @updateNeeded="setupPage"
:in-modal="true" :key="item"
/> :in-modal="true"
<EntityEdit />
v-else-if="loaded && (props.mode === 'edit' || props.mode === 'create')" <EntityEdit
:type="props.type" v-else-if="loaded && (props.mode === 'edit' || props.mode === 'create')"
:item="item" :type="props.type"
:inModal="true" :item="item"
@return-data="(data) => emit('return-data',data)" :inModal="true"
:createQuery="props.createQuery" @return-data="(data) => emit('return-data',data)"
:mode="props.mode" :createQuery="props.createQuery"
/> :mode="props.mode"
<!-- <EntityList />
v-else-if="loaded && props.mode === 'list'" <!-- <EntityList
:type="props.type" v-else-if="loaded && props.mode === 'list'"
:items="items" :type="props.type"
/>--> :items="items"
<UProgress />-->
v-else <UProgress
animation="carousel" v-else
class="p-5 mt-10" animation="carousel"
/> class="p-5 mt-10"
/>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -1,27 +1,59 @@
<script setup> <script setup>
const auth = useAuthStore() 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> </script>
<template> <template>
<USelectMenu <USelectMenu
:options="auth.tenants" :items="tenantItems"
value-attribute="id" :content="{ align: 'start', side: 'bottom', sideOffset: 6 }"
class="w-40" :ui="{ content: 'w-[var(--reka-dropdown-menu-trigger-width)] max-w-[var(--reka-dropdown-menu-trigger-width)]' }"
@change="auth.switchTenant(selectedTenant)" class="block w-40"
v-model="selectedTenant" :avatar="{
alt: activeTenantName,
text: tenantInitials,
loading: 'lazy'
}"
> >
<UButton color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" class="w-full"> <template #default="{ open }">
<UAvatar :alt="auth.activeTenantData?.name" size="md" /> <UButton
color="gray"
<span class="truncate text-gray-900 dark:text-white font-semibold">{{auth.tenants.find(i => auth.activeTenant === i.id).name}}</span> variant="ghost"
</UButton> 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']"
<template #option="{option}"> >
{{option.name}} <span class="min-w-0 flex-1 truncate font-medium text-gray-900 dark:text-white">
{{ activeTenantName }}
</span>
</UButton>
</template> </template>
</USelectMenu> </USelectMenu>
</template> </template>

View File

@@ -11,7 +11,7 @@
<slot name="right"/> <slot name="right"/>
</InputGroup> </InputGroup>
</div> </div>
<UDivider class="my-3"/> <USeparator class="my-3"/>
</template> </template>
<style scoped> <style scoped>

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

View File

@@ -1,57 +1,44 @@
<script setup> <script setup>
const { isHelpSlideoverOpen } = useDashboard()
const { isDashboardSearchModalOpen } = useUIState()
const { metaSymbol } = useShortcuts()
const auth = useAuthStore() const auth = useAuthStore()
const items = computed(() => [ const userItems = computed(() => [[
[{ {
slot: 'account', label: 'Passwort aendern',
label: '',
disabled: true
}], [/*{
label: 'Mein Profil',
icon: 'i-heroicons-user',
to: `/profiles/show/${profileStore.activeProfile.id}`
},*/{
label: 'Passwort ändern',
icon: 'i-heroicons-shield-check', icon: 'i-heroicons-shield-check',
to: `/password-change` to: '/password-change'
},{ },
{
label: 'Abmelden', label: 'Abmelden',
icon: 'i-heroicons-arrow-left-on-rectangle', icon: 'i-heroicons-arrow-left-on-rectangle',
click: async () => { onSelect: async () => {
await auth.logout() await auth.logout()
} }
}] }
]) ]])
</script> </script>
<template> <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 }"> <template #default="{ open }">
<UButton color="gray" variant="ghost" class="w-full" :label="auth.user.email" :class="[open && 'bg-gray-50 dark:bg-gray-800']"> <UButton
<!-- <template #leading> color="gray"
<UAvatar :alt="auth.user.email" size="xs" /> variant="ghost"
</template>--> class="w-full min-w-0 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">
{{ auth.user.email }}
</span>
<template #trailing> <template #trailing>
<UIcon name="i-heroicons-ellipsis-vertical" class="w-5 h-5 ml-auto" /> <UIcon name="i-heroicons-ellipsis-vertical" class="h-5 w-5 shrink-0" />
</template> </template>
</UButton> </UButton>
</template> </template>
</UDropdownMenu>
<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>
</template> </template>

View File

@@ -67,12 +67,13 @@ const startImport = () => {
<template> <template>
<UModal :fullscreen="false"> <UModal :fullscreen="false">
<template #content>
<UCard> <UCard>
<template #header> <template #header>
Erstelltes Dokument Kopieren Erstelltes Dokument Kopieren
</template> </template>
<UFormGroup <UFormField
label="Dokumententyp:" label="Dokumententyp:"
class="mb-3" class="mb-3"
> >
@@ -84,7 +85,7 @@ const startImport = () => {
> >
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UCheckbox <UCheckbox
v-for="key in Object.keys(optionsToImport).filter(i => documentTypeToUse !== props.type ? !['startText','endText'].includes(i) : true)" v-for="key in Object.keys(optionsToImport).filter(i => documentTypeToUse !== props.type ? !['startText','endText'].includes(i) : true)"
v-model="optionsToImport[key]" v-model="optionsToImport[key]"
@@ -101,9 +102,10 @@ const startImport = () => {
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@@ -243,26 +243,26 @@ loadData()
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<USelectMenu <USelectMenu
v-model="granularity" v-model="granularity"
:options="granularityOptions" :items="granularityOptions"
value-attribute="value" value-key="value"
option-attribute="label" label-key="label"
class="w-28" class="w-28"
/> />
<USelectMenu <USelectMenu
v-model="selectedYear" v-model="selectedYear"
:options="yearOptions" :items="yearOptions"
value-attribute="value" value-key="value"
option-attribute="label" label-key="label"
class="w-24" class="w-24"
/> />
<USelectMenu <USelectMenu
v-if="granularity === 'month'" v-if="granularity === 'month'"
v-model="selectedMonth" v-model="selectedMonth"
:options="monthOptions" :items="monthOptions"
value-attribute="value" value-key="value"
option-attribute="label" label-key="label"
class="w-36" class="w-36"
/> />
</div> </div>
@@ -288,26 +288,26 @@ loadData()
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<USelectMenu <USelectMenu
v-model="granularity" v-model="granularity"
:options="granularityOptions" :items="granularityOptions"
value-attribute="value" value-key="value"
option-attribute="label" label-key="label"
class="w-28" class="w-28"
/> />
<USelectMenu <USelectMenu
v-model="selectedYear" v-model="selectedYear"
:options="yearOptions" :items="yearOptions"
value-attribute="value" value-key="value"
option-attribute="label" label-key="label"
class="w-24" class="w-24"
/> />
<USelectMenu <USelectMenu
v-if="granularity === 'month'" v-if="granularity === 'month'"
v-model="selectedMonth" v-model="selectedMonth"
:options="monthOptions" :items="monthOptions"
value-attribute="value" value-key="value"
option-attribute="label" label-key="label"
class="w-36" class="w-36"
/> />
</div> </div>

View File

@@ -16,27 +16,23 @@ const setupPage = async () => {
let documents = items.filter(i => i.type === "invoices" ||i.type === "advanceInvoices") let documents = items.filter(i => i.type === "invoices" ||i.type === "advanceInvoices")
let draftDocuments = documents.filter(i => i.state === "Entwurf") let draftDocuments = documents.filter(i => i.state === "Entwurf")
let finalizedDocuments = documents.filter(i => i.state === "Gebucht") let finalizedDocuments = documents.filter(i => useSum().isOpenCreatedDocument(i, items))
finalizedDocuments = finalizedDocuments.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i, documents).toFixed(2))
finalizedDocuments = finalizedDocuments.filter(x => (x.type === 'invoices' || x.type === 'advanceInvoices') && x.state === 'Gebucht' && !items.find(i => i.createddocument && i.createddocument.id === x.id))
finalizedDocuments.forEach(i => { finalizedDocuments.forEach(i => {
if(dayjs().subtract(i.paymentDays,"days").isAfter(i.documentDate)) { if(dayjs().subtract(i.paymentDays,"days").isAfter(i.documentDate)) {
unpaidOverdueInvoicesSum.value += useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0) unpaidOverdueInvoicesSum.value += useSum().getCreatedDocumentOpenAmount(i, items)
unpaidOverdueInvoicesCount.value += 1 unpaidOverdueInvoicesCount.value += 1
} else { } else {
unpaidInvoicesSum.value += useSum().getCreatedDocumentSum(i, items) - i.statementallocations.reduce((n,{amount}) => n + amount, 0) unpaidInvoicesSum.value += useSum().getCreatedDocumentOpenAmount(i, items)
unpaidInvoicesCount.value += 1 unpaidInvoicesCount.value += 1
} }
}) })
//unpaidInvoicesCount.value = finalizedDocuments.length //unpaidInvoicesCount.value = finalizedDocuments.length
draftDocuments.forEach(i => { draftDocuments.forEach(i => {
draftInvoicesSum.value += useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0) draftInvoicesSum.value += useSum().getCreatedDocumentOpenAmount(i, items)
}) })
draftInvoicesCount.value = draftDocuments.length draftInvoicesCount.value = draftDocuments.length

View File

@@ -23,9 +23,9 @@ setupPage()
<template> <template>
<UTable <UTable
v-if="openTasks.length > 0" v-if="openTasks.length > 0"
:rows="openTasks" :data="openTasks"
:columns="[{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}]" :columns="normalizeTableColumns([{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}])"
@select="(i) => router.push(`/tasks/show/${i.id}`)" :on-select="(i) => router.push(`/tasks/show/${i.id}`)"
/> />
<div v-else> <div v-else>
<p class="text-center font-bold">Keine offenen Aufgaben</p> <p class="text-center font-bold">Keine offenen Aufgaben</p>

View File

@@ -29,7 +29,7 @@ const startTime = async () => {
await setupPage() await setupPage()
} catch (error) { } catch (error) {
console.log(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) { } catch (error) {
console.log(error) console.log(error)
let errorId = await useError().logError(`${JSON.stringify(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>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> <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" class="mt-2"
label="Notizen:" label="Notizen:"
> >
<UTextarea <UTextarea
v-model="runningTimeInfo.notes" v-model="runningTimeInfo.notes"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
class="mt-2" class="mt-2"
label="Projekt:" label="Projekt:"
> >
@@ -74,7 +74,7 @@ const stopStartedTime = async () => {
value-attribute="id" value-attribute="id"
option-attribute="name" option-attribute="name"
/> />
</UFormGroup> </UFormField>
<UButton <UButton
class="mt-3" class="mt-3"
@click="stopStartedTime" @click="stopStartedTime"

View File

@@ -25,7 +25,7 @@ const startTime = async () => {
await setupPage() await setupPage()
} catch (error) { } catch (error) {
console.log(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) { } catch (error) {
console.log(error) console.log(error)
let errorId = await useError().logError(`${JSON.stringify(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>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> <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" class="mt-2"
label="Notizen:" label="Notizen:"
> >
<UTextarea <UTextarea
v-model="runningTimeInfo.notes" v-model="runningTimeInfo.notes"
/> />
</UFormGroup> </UFormField>
<UButton <UButton
class="mt-3" class="mt-3"
@click="stopStartedTime" @click="stopStartedTime"

View File

@@ -0,0 +1,183 @@
<script setup lang="ts">
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
import {
formatTaxEvaluationPeriodLabel,
formatTaxEvaluationPeriodRange,
getCreatedDocumentTaxBreakdown,
getIncomingInvoiceTaxBreakdown,
getTaxEvaluationPeriodBounds,
normalizeTaxEvaluationPeriod
} from "~/composables/useTaxEvaluation"
dayjs.extend(customParseFormat)
const auth = useAuthStore()
const loading = ref(true)
const summary = ref({
label: "",
range: "",
outputTax: 0,
inputTax: 0,
balance: 0,
outputCount: 0,
inputCount: 0,
})
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR"
}).format(Number(value || 0))
}
const loadSummary = async () => {
loading.value = true
try {
const periodType = normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod)
const bounds = getTaxEvaluationPeriodBounds(dayjs(), periodType)
const [docs, incoming] = await Promise.all([
useEntities("createddocuments").select(),
useEntities("incominginvoices").select()
])
const outputDocs = (docs || []).filter((doc: any) => {
if (doc?.state !== "Gebucht") return false
if (!["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)) 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 (invoice?.state !== "Gebucht" || !invoice?.date) return false
const date = dayjs(invoice.date)
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
})
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)
summary.value = {
label: formatTaxEvaluationPeriodLabel(bounds.start, periodType),
range: formatTaxEvaluationPeriodRange(bounds.start, periodType),
outputTax: Number(outputTax.toFixed(2)),
inputTax: Number(inputTax.toFixed(2)),
balance: Number((outputTax - inputTax).toFixed(2)),
outputCount: outputDocs.length,
inputCount: inputDocs.length,
}
} finally {
loading.value = false
}
}
onMounted(loadSummary)
</script>
<template>
<div class="space-y-3">
<div class="tax-summary-top">
<div>
<p class="tax-summary-period">{{ summary.label }}</p>
<p class="tax-summary-range">{{ summary.range }}</p>
</div>
<UButton
size="xs"
variant="soft"
color="gray"
icon="i-heroicons-arrow-top-right-on-square"
@click="navigateTo('/accounting/tax')"
>
Details
</UButton>
</div>
<div class="tax-summary-row">
<span class="tax-summary-label">USt Rechnungen</span>
<span class="tax-summary-value text-amber-600 dark:text-amber-400">
{{ loading ? "..." : formatCurrency(summary.outputTax) }}
</span>
</div>
<div class="tax-summary-row">
<span class="tax-summary-label">Vorsteuer</span>
<span class="tax-summary-value text-sky-600 dark:text-sky-400">
{{ loading ? "..." : formatCurrency(summary.inputTax) }}
</span>
</div>
<div class="tax-summary-row">
<span class="tax-summary-label">Ergebnis</span>
<span
class="tax-summary-value"
:class="summary.balance >= 0 ? 'text-rose-600 dark:text-rose-400' : 'text-emerald-600 dark:text-emerald-400'"
>
{{ loading ? "..." : formatCurrency(summary.balance) }}
</span>
</div>
<div class="tax-summary-meta">
{{ summary.outputCount }} Ausgangsbelege | {{ summary.inputCount }} Eingangsbelege
</div>
</div>
</template>
<style scoped>
.tax-summary-top {
display: flex;
justify-content: space-between;
gap: 0.75rem;
align-items: flex-start;
}
.tax-summary-period {
margin: 0;
font-weight: 700;
color: rgb(17 24 39);
}
.tax-summary-range,
.tax-summary-meta {
margin: 0;
font-size: 0.875rem;
color: rgb(107 114 128);
}
.tax-summary-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
}
.tax-summary-label {
color: rgb(55 65 81);
}
.tax-summary-value {
font-weight: 700;
text-align: right;
}
:deep(.dark) .tax-summary-period {
color: rgb(243 244 246);
}
:deep(.dark) .tax-summary-range,
:deep(.dark) .tax-summary-meta,
:deep(.dark) .tax-summary-label {
color: rgb(156 163 175);
}
</style>

View File

@@ -12,6 +12,10 @@ const props = defineProps({
const products = ref([]) const products = ref([])
const units = ref([]) const units = ref([])
const productSearchInput = {
placeholder: 'Artikel suchen...'
}
const setup = async () => { const setup = async () => {
products.value = await useEntities("products").select() products.value = await useEntities("products").select()
units.value = await useEntities("units").selectSpecial() units.value = await useEntities("units").selectSpecial()
@@ -80,16 +84,16 @@ const setRowData = (row) => {
> >
<td> <td>
<USelectMenu <USelectMenu
searchable :items="products"
:search-attributes="['name']" label-key="name"
:options="products" value-key="id"
value-attribute="id" :search-input="productSearchInput"
option-attribute="name" :filter-fields="['name']"
v-model="product.product" v-model="product.product"
:color="product.product ? 'primary' : 'rose'" :color="product.product ? 'primary' : 'error'"
@change="setRowData(product)" @change="setRowData(product)"
> >
<template #label> <template #default>
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}} {{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
</template> </template>
</USelectMenu> </USelectMenu>
@@ -104,9 +108,9 @@ const setRowData = (row) => {
</td> </td>
<td> <td>
<USelectMenu <USelectMenu
:options="units" :items="units"
value-attribute="id" label-key="name"
option-attribute="name" value-key="id"
v-model="product.unit" v-model="product.unit"
></USelectMenu> ></USelectMenu>
</td> </td>
@@ -123,7 +127,7 @@ const setRowData = (row) => {
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="removeProductFromMaterialComposition(product.id)" @click="removeProductFromMaterialComposition(product.id)"
variant="outline" variant="outline"
color="rose" color="error"
/> />
</td> </td>
</tr> </tr>
@@ -135,4 +139,4 @@ const setRowData = (row) => {
td { td {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
</style> </style>

View File

@@ -53,7 +53,10 @@ const emit = defineEmits(["click"])
<style scoped> <style scoped>
/* FAB Basis */ /* FAB Basis */
.fab-base { .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 nur ein Icon vorhanden ist → runder Kreis */
/* Wenn Label + Icon → Extended FAB */ /* Wenn Label + Icon → Extended FAB */
@@ -61,6 +64,12 @@ const emit = defineEmits(["click"])
/* Optional: Auto-Kreisen wenn kein Label */ /* Optional: Auto-Kreisen wenn kein Label */
#fab:not([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> </style>

View File

@@ -47,14 +47,14 @@ async function handlePrint() {
{{labelPrinter.printProgress}} {{labelPrinter.printProgress}}
<UFormGroup label="Breite"> <UFormField label="Breite">
<UInput v-model="labelWidth"><template #trailing>mm</template></UInput> <UInput v-model="labelWidth"><template #trailing>mm</template></UInput>
</UFormGroup> </UFormField>
<UFormGroup label="Höhe"> <UFormField label="Höhe">
<UInput v-model="labelHeight"><template #trailing>mm</template></UInput> <UInput v-model="labelHeight"><template #trailing>mm</template></UInput>
</UFormGroup> </UFormField>
<UFormGroup label="ZPL"> <UFormField label="ZPL">
<UTextarea v-model="zpl" rows="6" /> <UTextarea v-model="zpl" rows="6" />
</UFormGroup> </UFormField>
</UCard> </UCard>
</template> </template>

View File

@@ -12,6 +12,10 @@ const props = defineProps({
const hourrates = ref([]) const hourrates = ref([])
const units = ref([]) const units = ref([])
const hourrateSearchInput = {
placeholder: 'Stundensatz suchen...'
}
const setup = async () => { const setup = async () => {
hourrates.value = await useEntities("hourrates").select() hourrates.value = await useEntities("hourrates").select()
units.value = await useEntities("units").selectSpecial() units.value = await useEntities("units").selectSpecial()
@@ -82,14 +86,14 @@ const setRowData = (row) => {
> >
<td> <td>
<USelectMenu <USelectMenu
searchable :items="hourrates"
:search-attributes="['name']" label-key="name"
:options="hourrates" value-key="id"
value-attribute="id" :search-input="hourrateSearchInput"
option-attribute="name" :filter-fields="['name']"
v-model="row.hourrate" v-model="row.hourrate"
:color="row.hourrate ? 'primary' : 'rose'" :color="row.hourrate ? 'primary' : 'error'"
@change="setRowData(row)" @change="setRowData(row)"
> >
<!-- <template #label> <!-- <template #label>
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}} {{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
@@ -106,10 +110,10 @@ const setRowData = (row) => {
</td> </td>
<td> <td>
<USelectMenu <USelectMenu
:options="units" :items="units"
disabled disabled
value-attribute="id" label-key="name"
option-attribute="name" value-key="id"
v-model="row.unit" v-model="row.unit"
></USelectMenu> ></USelectMenu>
</td> </td>
@@ -134,7 +138,7 @@ const setRowData = (row) => {
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="removeRowFromPersonalComposition(row.id)" @click="removeRowFromPersonalComposition(row.id)"
variant="outline" variant="outline"
color="rose" color="error"
/> />
</td> </td>
</tr> </tr>
@@ -146,4 +150,4 @@ const setRowData = (row) => {
td { td {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
</style> </style>

View File

@@ -206,16 +206,62 @@ const addVideo = () => {
<style scoped> <style scoped>
/* Toolbar & Buttons */ /* Toolbar & Buttons */
.toolbar-btn { .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 { .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 { .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 { .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 */ /* GLOBAL EDITOR STYLES */
@@ -235,20 +281,48 @@ const addVideo = () => {
/* MENTION */ /* MENTION */
.wiki-mention { .wiki-mention {
/* Pill-Shape, grau/neutral statt knallig blau */ /* 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; box-decoration-break: clone;
} }
.wiki-mention::before { .wiki-mention::before {
@apply text-gray-400 dark:text-gray-500 mr-0.5; color: #9ca3af;
margin-right: 0.125rem;
} }
.wiki-mention:hover { .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; 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 */
table { width: 100% !important; border-collapse: collapse; table-layout: fixed; margin: 1rem 0; } 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; } 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; } .column-resize-handle { background-color: #3b82f6; width: 4px; }
/* CODE */ /* 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; } code { color: inherit; padding: 0; background: none; font-size: 0.9rem; }
/* IMG */ /* IMG */
@@ -269,4 +343,4 @@ const addVideo = () => {
mark { background-color: #fef08a; padding: 0.1rem 0.2rem; border-radius: 0.2rem; } mark { background-color: #fef08a; padding: 0.1rem 0.2rem; border-radius: 0.2rem; }
.ProseMirror-selectednode { outline: 2px solid #3b82f6; } .ProseMirror-selectednode { outline: 2px solid #3b82f6; }
} }
</style> </style>

View File

@@ -98,17 +98,19 @@
</div> </div>
</div> </div>
<UModal v-model="isCreateModalOpen"> <UModal v-model:open="isCreateModalOpen">
<div class="p-5"> <template #content>
<h3 class="font-bold mb-4">Neue Seite</h3> <div class="p-5">
<form @submit.prevent="createPage"> <h3 class="font-bold mb-4">Neue Seite</h3>
<UInput v-model="newTitle" placeholder="Titel..." autofocus /> <form @submit.prevent="createPage">
<div class="mt-4 flex justify-end gap-2"> <UInput v-model="newTitle" placeholder="Titel..." autofocus />
<UButton color="gray" variant="ghost" @click="isCreateModalOpen = false">Abbrechen</UButton> <div class="mt-4 flex justify-end gap-2">
<UButton type="submit" color="primary" :loading="isCreating">Erstellen</UButton> <UButton color="gray" variant="ghost" @click="isCreateModalOpen = false">Abbrechen</UButton>
</div> <UButton type="submit" color="primary" :loading="isCreating">Erstellen</UButton>
</form> </div>
</div> </form>
</div>
</template>
</UModal> </UModal>
</div> </div>
@@ -163,7 +165,7 @@ async function selectPage(id: string) {
const data = await $api(`/api/wiki/${id}`, { method: 'GET' }) const data = await $api(`/api/wiki/${id}`, { method: 'GET' })
selectedPage.value = data selectedPage.value = data
} catch (e) { } catch (e) {
toast.add({ title: 'Fehler beim Laden', color: 'red' }) toast.add({ title: 'Fehler beim Laden', color: 'error' })
} finally { } finally {
loadingContent.value = false loadingContent.value = false
} }
@@ -233,4 +235,4 @@ watch(() => [props.entityId, props.entityUuid], fetchList)
<style scoped> <style scoped>
.custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-thumb { background-color: #ddd; border-radius: 4px; } .custom-scrollbar::-webkit-scrollbar-thumb { background-color: #ddd; border-radius: 4px; }
</style> </style>

View File

@@ -0,0 +1,155 @@
import { createSharedComposable } from '@vueuse/core'
type ChangelogEntry = {
hash: string
shortHash: string
subject: string
authorName: string
committedAt: string
}
type ChangelogSeenState = {
lastOpenedAt: string | null
latestSeenHash: string | null
}
const defaultSeenState = (): ChangelogSeenState => ({
lastOpenedAt: null,
latestSeenHash: null
})
let changelogRequest: Promise<void> | null = null
const _useChangelog = () => {
const auth = useAuthStore()
const entries = useState<ChangelogEntry[]>('changelog:entries', () => [])
const pending = useState<boolean>('changelog:pending', () => false)
const error = useState<string | null>('changelog:error', () => null)
const loadedKey = useState<string | null>('changelog:loaded-key', () => null)
const seenState = useState<ChangelogSeenState>('changelog:seen-state', defaultSeenState)
const scopeKey = computed(() => {
const userId = auth.user?.id
const tenantId = auth.activeTenant
if (!userId || !tenantId) return null
return `${userId}:${tenantId}`
})
const storageKey = computed(() => {
if (!scopeKey.value) return null
return `fedeo:changelog:last-opened:${scopeKey.value}`
})
const latestEntry = computed(() => entries.value[0] || null)
const hasUnread = computed(() => {
if (!latestEntry.value?.hash) return false
return latestEntry.value.hash !== seenState.value.latestSeenHash
})
function loadSeenState() {
if (!process.client || !storageKey.value) {
seenState.value = defaultSeenState()
return
}
try {
const raw = localStorage.getItem(storageKey.value)
if (!raw) {
seenState.value = defaultSeenState()
return
}
const parsed = JSON.parse(raw)
seenState.value = {
lastOpenedAt: parsed?.lastOpenedAt || null,
latestSeenHash: parsed?.latestSeenHash || null
}
} catch (err) {
console.error('Could not parse changelog seen state', err)
seenState.value = defaultSeenState()
}
}
async function refresh(force = false) {
if (!process.client || !scopeKey.value) return
if (!force && loadedKey.value === scopeKey.value && entries.value.length) return
if (changelogRequest) return changelogRequest
changelogRequest = (async () => {
pending.value = true
error.value = null
try {
const response = await useNuxtApp().$api('/api/functions/changelog', {
query: { limit: 20 }
})
entries.value = Array.isArray(response?.entries) ? response.entries : []
loadedKey.value = scopeKey.value
} catch (err: any) {
error.value = err?.data?.error || err?.message || 'Changelog konnte nicht geladen werden.'
} finally {
pending.value = false
}
})()
try {
await changelogRequest
} finally {
changelogRequest = null
}
}
function markAsSeen() {
if (!process.client || !storageKey.value) return
const nextState = {
lastOpenedAt: new Date().toISOString(),
latestSeenHash: latestEntry.value?.hash || null
}
seenState.value = nextState
try {
localStorage.setItem(storageKey.value, JSON.stringify(nextState))
} catch (err) {
console.error('Could not persist changelog seen state', err)
}
}
watch(storageKey, () => {
loadSeenState()
}, { immediate: true })
watch(scopeKey, (nextScopeKey, previousScopeKey) => {
if (!process.client || !nextScopeKey) return
if (nextScopeKey !== previousScopeKey) {
entries.value = []
loadedKey.value = null
}
void refresh(true)
}, { immediate: true })
return {
entries,
pending,
error,
latestEntry,
hasUnread,
seenState,
refresh,
markAsSeen
}
}
export const useChangelog = createSharedComposable(_useChangelog)

View File

@@ -36,6 +36,9 @@ export const useFiles = () => {
let data = [] let data = []
data = await useEntities("files").select("*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)") data = await useEntities("files").select("*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)")
if (!Array.isArray(data) || data.length === 0) {
return []
}
const res = await useNuxtApp().$api("/api/files/presigned",{ const res = await useNuxtApp().$api("/api/files/presigned",{
@@ -138,4 +141,4 @@ export const useFiles = () => {
return {uploadFiles, selectDocuments, selectSomeDocuments, selectDocument, downloadFile, dataURLtoFile} return {uploadFiles, selectDocuments, selectSomeDocuments, selectDocument, downloadFile, dataURLtoFile}
} }

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

View File

@@ -1,5 +1,16 @@
export const useSum = () => { export const useSum = () => {
const unwrapCreatedDocuments = (createddocuments = []) => {
if (Array.isArray(createddocuments)) return createddocuments
if (Array.isArray(createddocuments?.value)) return createddocuments.value
return []
}
const getCreatedDocumentLinkId = (value) => {
if (value && typeof value === "object") return value.id
return value
}
const getIncomingInvoiceSum = (invoice) => { const getIncomingInvoiceSum = (invoice) => {
let sum = 0 let sum = 0
invoice.accounts.forEach(account => { invoice.accounts.forEach(account => {
@@ -15,6 +26,7 @@ export const useSum = () => {
} }
const getCreatedDocumentSum = (createddocument,createddocuments = []) => { const getCreatedDocumentSum = (createddocument,createddocuments = []) => {
const availableCreatedDocuments = unwrapCreatedDocuments(createddocuments)
let totalNet = 0 let totalNet = 0
let total19 = 0 let total19 = 0
let total7 = 0 let total7 = 0
@@ -44,7 +56,9 @@ export const useSum = () => {
createddocument.usedAdvanceInvoices.forEach(advanceInvoiceId => { createddocument.usedAdvanceInvoices.forEach(advanceInvoiceId => {
let advanceInvoice = createddocuments.find(i => i.id === advanceInvoiceId) let advanceInvoice = availableCreatedDocuments.find(i => i.id === advanceInvoiceId)
if (!advanceInvoice) return
let priceNet = advanceInvoice.rows.find(i => i.advanceInvoiceData).price let priceNet = advanceInvoice.rows.find(i => i.advanceInvoiceData).price
@@ -59,6 +73,24 @@ export const useSum = () => {
return Number(sumToPay.toFixed(2)) return Number(sumToPay.toFixed(2))
} }
const hasCancellationInvoice = (createddocument, createddocuments = []) => {
const availableCreatedDocuments = unwrapCreatedDocuments(createddocuments)
return availableCreatedDocuments.some((document) => {
return document.type === "cancellationInvoices"
&& document.state !== "Entwurf"
&& !document.archived
&& getCreatedDocumentLinkId(document.createddocument) === createddocument.id
})
}
const getCreatedDocumentOpenAmount = (createddocument, createddocuments = []) => {
let amountPaid = 0
createddocument.statementallocations.forEach(allocation => amountPaid += allocation.amount)
return Number((getCreatedDocumentSum(createddocument, createddocuments) - amountPaid).toFixed(2))
}
const getCreatedDocumentSumDetailed = (createddocument) => { const getCreatedDocumentSumDetailed = (createddocument) => {
let totalNet = 0 let totalNet = 0
let total19 = 0 let total19 = 0
@@ -124,12 +156,24 @@ export const useSum = () => {
} }
const getIsPaid = (createddocument,createddocuments) => { const getIsPaid = (createddocument,createddocuments) => {
let amountPaid = 0 return getCreatedDocumentOpenAmount(createddocument, createddocuments) === 0
createddocument.statementallocations.forEach(allocation => amountPaid += allocation.amount)
return Number(amountPaid.toFixed(2)) === getCreatedDocumentSum(createddocument,createddocuments)
} }
return {getIncomingInvoiceSum, getCreatedDocumentSum, getCreatedDocumentSumDetailed, getIsPaid} const isOpenCreatedDocument = (createddocument, createddocuments = []) => {
return ['invoices', 'advanceInvoices'].includes(createddocument.type)
&& createddocument.state === "Gebucht"
&& !hasCancellationInvoice(createddocument, createddocuments)
&& !getIsPaid(createddocument, createddocuments)
}
return {
getIncomingInvoiceSum,
getCreatedDocumentSum,
getCreatedDocumentSumDetailed,
getCreatedDocumentOpenAmount,
getIsPaid,
hasCancellationInvoice,
isOpenCreatedDocument
}
} }

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

View File

@@ -0,0 +1,162 @@
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
dayjs.extend(customParseFormat)
export const TAX_EVALUATION_PERIOD_OPTIONS = [
{ label: "Monatlich", value: "monthly" },
{ label: "Quartalsweise", value: "quarterly" },
{ label: "Jährlich", value: "yearly" },
]
export const normalizeTaxEvaluationPeriod = (value?: string) => {
if (value === "quarterly" || value === "yearly") return value
return "monthly"
}
const ZERO_BREAKDOWN = () => ({
net19: 0,
tax19: 0,
net7: 0,
tax7: 0,
net0: 0,
})
const isTaxFreeDocument = (taxType?: string | null) => {
return ["13b UStG", "19 UStG", "12.3 UStG"].includes(String(taxType || ""))
}
export const getTaxEvaluationPeriodBounds = (
referenceDate: dayjs.ConfigType,
period: string
) => {
const normalized = normalizeTaxEvaluationPeriod(period)
const base = dayjs(referenceDate)
if (normalized === "yearly") {
return {
start: base.startOf("year"),
end: base.endOf("year"),
}
}
if (normalized === "quarterly") {
const quarterStartMonth = Math.floor(base.month() / 3) * 3
const start = base.month(quarterStartMonth).startOf("month")
return {
start,
end: start.add(2, "month").endOf("month"),
}
}
return {
start: base.startOf("month"),
end: base.endOf("month"),
}
}
export const shiftTaxEvaluationPeriodStart = (
periodStart: dayjs.ConfigType,
period: string,
offset: number
) => {
const normalized = normalizeTaxEvaluationPeriod(period)
const base = dayjs(periodStart)
if (normalized === "yearly") return base.add(offset, "year").startOf("year")
if (normalized === "quarterly") return base.add(offset * 3, "month").startOf("month")
return base.add(offset, "month").startOf("month")
}
export const formatTaxEvaluationPeriodLabel = (
periodStart: dayjs.ConfigType,
period: string
) => {
const { start, end } = getTaxEvaluationPeriodBounds(periodStart, period)
const normalized = normalizeTaxEvaluationPeriod(period)
if (normalized === "yearly") {
return start.format("YYYY")
}
if (normalized === "quarterly") {
return `Q${Math.floor(start.month() / 3) + 1} ${start.format("YYYY")}`
}
return start.format("MMMM YYYY")
}
export const formatTaxEvaluationPeriodRange = (
periodStart: dayjs.ConfigType,
period: string
) => {
const { start, end } = getTaxEvaluationPeriodBounds(periodStart, period)
return `${start.format("DD.MM.YYYY")} - ${end.format("DD.MM.YYYY")}`
}
export const getCreatedDocumentTaxBreakdown = (doc: any) => {
const breakdown = ZERO_BREAKDOWN()
if (!doc || isTaxFreeDocument(doc.taxType)) {
return breakdown
}
;(doc.rows || []).forEach((row: any) => {
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) return
const quantity = Number(row.quantity || 0)
const price = Number(row.price || 0)
const discountPercent = Number(row.discountPercent || 0)
const taxPercent = Number(row.taxPercent || 0)
const net = Number((quantity * price * (1 - discountPercent / 100)).toFixed(2))
if (!Number.isFinite(net) || net === 0) return
if (taxPercent === 19) {
breakdown.net19 += net
breakdown.tax19 += Number((net * 0.19).toFixed(2))
} else if (taxPercent === 7) {
breakdown.net7 += net
breakdown.tax7 += Number((net * 0.07).toFixed(2))
} else {
breakdown.net0 += net
}
})
return {
net19: Number(breakdown.net19.toFixed(2)),
tax19: Number(breakdown.tax19.toFixed(2)),
net7: Number(breakdown.net7.toFixed(2)),
tax7: Number(breakdown.tax7.toFixed(2)),
net0: Number(breakdown.net0.toFixed(2)),
}
}
export const getIncomingInvoiceTaxBreakdown = (invoice: any) => {
const breakdown = ZERO_BREAKDOWN()
;(invoice?.accounts || []).forEach((account: any) => {
const taxType = String(account?.taxType || "")
const amountNet = Number(account?.amountNet || 0)
const amountTax = Number(account?.amountTax || 0)
if (taxType === "19") {
breakdown.net19 += amountNet
breakdown.tax19 += amountTax
} else if (taxType === "7") {
breakdown.net7 += amountNet
breakdown.tax7 += amountTax
} else {
breakdown.net0 += amountNet
}
})
return {
net19: Number(breakdown.net19.toFixed(2)),
tax19: Number(breakdown.tax19.toFixed(2)),
net7: Number(breakdown.net7.toFixed(2)),
tax7: Number(breakdown.tax7.toFixed(2)),
net0: Number(breakdown.net0.toFixed(2)),
}
}

View File

@@ -16,6 +16,7 @@ const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const labelPrinter = useLabelPrinterStore() const labelPrinter = useLabelPrinterStore()
const calculatorStore = useCalculatorStore() const calculatorStore = useCalculatorStore()
const { hasUnread, refresh: refreshChangelog } = useChangelog()
const month = dayjs().format("MM") const month = dayjs().format("MM")
@@ -114,7 +115,7 @@ const groups = computed(() => [
].filter(Boolean)) ].filter(Boolean))
// --- Footer Links nutzen jetzt den zentralen Calculator Store --- // --- Footer Links nutzen jetzt den zentralen Calculator Store ---
const footerLinks = computed(() => [ const footerItems = computed(() => [
{ {
label: 'Taschenrechner', label: 'Taschenrechner',
icon: 'i-heroicons-calculator', icon: 'i-heroicons-calculator',
@@ -123,10 +124,15 @@ const footerLinks = computed(() => [
{ {
label: 'Hilfe & Info', label: 'Hilfe & Info',
icon: 'i-heroicons-question-mark-circle', icon: 'i-heroicons-question-mark-circle',
badge: hasUnread.value ? 'Neu' : null,
click: () => isHelpSlideoverOpen.value = true click: () => isHelpSlideoverOpen.value = true
} }
]) ])
onMounted(() => {
void refreshChangelog()
})
</script> </script>
<template> <template>
@@ -234,48 +240,81 @@ const footerLinks = computed(() => [
</UCard> </UCard>
</UContainer> </UContainer>
</div> </div>
<UDashboardLayout class="safearea" v-else> <div class="safearea flex min-h-screen w-full flex-col overflow-hidden" v-else>
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible> <!-- <div
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;" class="border-b border-default bg-default px-3 py-2"
:class="['!border-transparent']" :ui="{ left: 'flex-1' }"> style="padding-top: max(env(safe-area-inset-top, 0px), 0.5rem);"
<template #left> >
<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"/> <TenantDropdown class="w-full"/>
</template> </template>
</UDashboardNavbar>
<UDashboardSidebar id="sidebar"> <template #default="{ collapsed }">
<MainNav :collapsed="collapsed" />
<MainNav/> </template>
<div class="flex-1"/>
<template #footer>
<template #footer="{ collapsed }">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<UColorModeSwitch :class="[collapsed ? 'mx-auto' : 'ml-3']"/>
<UDashboardSidebarCollapse v-if="collapsed" class="mx-auto" />
</div>
<UColorModeToggle class="ml-3"/>
<LabelPrinterButton class="w-full"/> <LabelPrinterButton class="w-full"/>
<UDashboardSidebarLinks :links="footerLinks" class="w-full"/> <div class="flex flex-col gap-1">
<UButton
v-for="item in footerItems"
:key="item.label"
color="gray"
variant="ghost"
class="w-full"
:icon="item.icon"
@click="item.click ? item.click() : null"
>
<span v-if="!collapsed">{{ item.label }}</span>
<UDivider class="sticky bottom-0 w-full"/> <template #trailing>
<UBadge v-if="!collapsed && item.badge" color="primary" variant="solid" size="xs">
{{ item.badge }}
</UBadge>
</template>
</UButton>
</div>
<USeparator class="sticky bottom-0 w-full"/>
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;" class="w-full"/> <UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;" class="w-full"/>
</div> </div>
</template> </template>
</UDashboardSidebar> </UDashboardSidebar>
</UDashboardPanel>
<UDashboardPage>
<UDashboardPanel grow> <div class="flex min-w-0 flex-1 flex-col overflow-hidden">
<slot/> <slot/>
</UDashboardPanel>
</UDashboardPage> </div>
</UDashboardGroup>
<HelpSlideover/> <HelpSlideover/>
<Calculator v-if="calculatorStore.isOpen"/> <Calculator v-if="calculatorStore.isOpen"/>
</div>
</UDashboardLayout>
</div> </div>
<div <div
@@ -306,7 +345,7 @@ const footerLinks = computed(() => [
</div> </div>
<UButton <UButton
variant="outline" variant="outline"
color="rose" color="error"
@click="auth.logout()" @click="auth.logout()"
>Abmelden >Abmelden
</UButton> </UButton>

View File

@@ -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, ssr: false,
@@ -15,14 +15,12 @@ export default defineNuxtConfig({
dirs: ['stores'] dirs: ['stores']
}, },
extends: [
'@nuxt/ui-pro'
],
components: [{ components: [{
path: '~/components' path: '~/components'
}], }],
css: ['~/assets/css/main.css'],
build: { build: {
transpile: ['@vuepic/vue-datepicker','@tiptap/vue-3','@tiptap/extension-code-block-lowlight', transpile: ['@vuepic/vue-datepicker','@tiptap/vue-3','@tiptap/extension-code-block-lowlight',
'lowlight',] 'lowlight',]
@@ -74,10 +72,6 @@ export default defineNuxtConfig({
}, },
}, },
ui: {
icons: ['heroicons', 'mdi', 'simple-icons']
},
colorMode: { colorMode: {
preference: 'system' preference: 'system'
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,8 @@
"@fullcalendar/vue3": "^6.1.10", "@fullcalendar/vue3": "^6.1.10",
"@iconify/json": "^2.2.171", "@iconify/json": "^2.2.171",
"@mmote/niimbluelib": "^0.0.1-alpha.29", "@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/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.1.0", "@nuxtjs/google-fonts": "^3.1.0",
"@nuxtjs/strapi": "^1.9.3", "@nuxtjs/strapi": "^1.9.3",

View File

@@ -0,0 +1,289 @@
<script setup lang="ts">
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
import {
formatTaxEvaluationPeriodLabel,
formatTaxEvaluationPeriodRange,
getCreatedDocumentTaxBreakdown,
getIncomingInvoiceTaxBreakdown,
getTaxEvaluationPeriodBounds,
normalizeTaxEvaluationPeriod,
shiftTaxEvaluationPeriodStart
} from "~/composables/useTaxEvaluation"
dayjs.extend(customParseFormat)
const auth = useAuthStore()
const loading = ref(true)
const createdDocuments = ref<any[]>([])
const incomingInvoices = ref<any[]>([])
const periodType = computed(() => normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod))
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 loadData = async () => {
loading.value = true
try {
const [docs, incoming] = await Promise.all([
useEntities("createddocuments").select(),
useEntities("incominginvoices").select()
])
createdDocuments.value = (docs || []).filter(isRelevantOutputDocument)
incomingInvoices.value = (incoming || []).filter(isRelevantInputInvoice)
} finally {
loading.value = false
}
}
const periods = computed(() => {
const currentBounds = getTaxEvaluationPeriodBounds(dayjs(), periodType.value)
return Array.from({ length: 8 }, (_, index) => {
const start = shiftTaxEvaluationPeriodStart(currentBounds.start, periodType.value, -index)
const bounds = getTaxEvaluationPeriodBounds(start, periodType.value)
const outputDocs = createdDocuments.value.filter((doc) => {
const date = dayjs(doc.documentDate)
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
})
const inputDocs = incomingInvoices.value.filter((invoice) => {
const date = dayjs(invoice.date)
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
})
const output = outputDocs.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 = inputDocs.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))
const balance = Number((outputTax - inputTax).toFixed(2))
return {
key: bounds.start.format("YYYY-MM-DD"),
label: formatTaxEvaluationPeriodLabel(bounds.start, periodType.value),
range: formatTaxEvaluationPeriodRange(bounds.start, periodType.value),
isCurrent: index === 0,
outputTax,
inputTax,
balance,
output,
input,
outputCount: outputDocs.length,
inputCount: inputDocs.length,
}
})
})
const currentPeriod = computed(() => periods.value[0] || null)
const columns = [
{ key: "label", label: "Zeitraum" },
{ key: "range", label: "Datumsbereich" },
{ key: "outputTax", label: "USt Rechnungen" },
{ key: "inputTax", label: "Vorsteuer" },
{ key: "balance", label: "Ergebnis" },
{ key: "documents", label: "Belege" },
]
onMounted(loadData)
</script>
<template>
<div>
<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'"
>
{{ 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">
<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>
<template #outputTax-cell="{ row }">
{{ formatCurrency(row.original.outputTax) }}
</template>
<template #inputTax-cell="{ row }">
{{ formatCurrency(row.original.inputTax) }}
</template>
<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 #documents-cell="{ row }">
{{ row.original.outputCount }} / {{ row.original.inputCount }}
</template>
</UTable>
</UCard>
</UDashboardPanelContent>
</div>
</template>

View File

@@ -158,7 +158,7 @@ setupPage()
<UButton <UButton
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
variant="outline" variant="outline"
color="rose" color="error"
@click="clearSearchString()" @click="clearSearchString()"
v-if="searchString.length > 0" v-if="searchString.length > 0"
/> />
@@ -194,20 +194,20 @@ setupPage()
</template> </template>
</UDashboardToolbar> </UDashboardToolbar>
<UTable <UTable
:rows="filteredRows" :data="filteredRows"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/accounts/show/${i.id}`)" :on-select="(i) => router.push(`/accounts/show/${i.id}`)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
> >
<template #allocations-data="{row}"> <template #allocations-cell="{row}">
<span v-if="dataLoaded">{{row.allocations ? row.allocations : null}}</span> <span v-if="dataLoaded">{{row.original.allocations ? row.original.allocations : null}}</span>
<USkeleton v-else class="h-4 w-[250px]" /> <USkeleton v-else class="h-4 w-[250px]" />
</template> </template>
<template #saldo-data="{row}"> <template #saldo-cell="{row}">
<span v-if="dataLoaded">{{row.allocations ? useCurrency(row.saldo) : null}}</span> <span v-if="dataLoaded">{{row.original.allocations ? useCurrency(row.original.saldo) : null}}</span>
<USkeleton v-else class="h-4 w-[250px]" /> <USkeleton v-else class="h-4 w-[250px]" />
</template> </template>
</UTable> </UTable>
@@ -219,4 +219,4 @@ setupPage()
<style scoped> <style scoped>
</style> </style>

View File

@@ -106,7 +106,7 @@ const saldo = computed(() => {
</UDashboardNavbar> </UDashboardNavbar>
<UDashboardPanelContent> <UDashboardPanelContent>
<UTabs :items="[{label: 'Information'},{label: 'Buchungen'}]"> <UTabs :items="[{label: 'Information'},{label: 'Buchungen'}]">
<template #item="{item}"> <template #content="{item}">
<UCard class="mt-5" v-if="item.label === 'Information'"> <UCard class="mt-5" v-if="item.label === 'Information'">
<div class="text-wrap"> <div class="text-wrap">
<table class="w-full" v-if="itemInfo"> <table class="w-full" v-if="itemInfo">
@@ -137,21 +137,21 @@ const saldo = computed(() => {
<UCard class="mt-5" v-if="item.label === 'Buchungen'"> <UCard class="mt-5" v-if="item.label === 'Buchungen'">
<UTable <UTable
v-if="statementallocations" v-if="statementallocations"
:rows="renderedAllocations" :data="renderedAllocations"
:columns="[{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}]" :columns="normalizeTableColumns([{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}])"
@select="(i) => selectAllocation(i)" :on-select="(i) => selectAllocation(i)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
> >
<template #amount-data="{row}"> <template #amount-cell="{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-rose-600" 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.amount > 0 || row.color === 'green'">{{useCurrency(row.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.amount)}}</span> <span v-else>{{useCurrency(row.original.amount)}}</span>
</template> </template>
<template #date-data="{row}"> <template #date-cell="{row}">
{{row.date ? dayjs(row.date).format('DD.MM.YYYY') : ''}} {{row.original.date ? dayjs(row.original.date).format('DD.MM.YYYY') : ''}}
</template> </template>
<template #description-data="{row}"> <template #description-cell="{row}">
{{row.description ? row.description : ''}} {{row.original.description ? row.original.description : ''}}
</template> </template>
</UTable> </UTable>
</UCard> </UCard>
@@ -167,4 +167,4 @@ td {
padding-bottom: 0.15em; padding-bottom: 0.15em;
padding-top: 0.15em; padding-top: 0.15em;
} }
</style> </style>

View File

@@ -67,12 +67,12 @@ const setupPage = async () => {
incominginvoices.value = invoiceItems.filter(i => i.state === "Gebucht") incominginvoices.value = invoiceItems.filter(i => i.state === "Gebucht")
const documents = createddocuments.value.filter(i => i.type === "invoices" || i.type === "advanceInvoices") const documents = createddocuments.value.filter(i => i.type === "invoices" || i.type === "advanceInvoices")
openDocuments.value = documents.filter(i => i.statementallocations.reduce((n, {amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i, createddocuments.value).toFixed(2)) openDocuments.value = documents.filter(i => useSum().isOpenCreatedDocument(i, createddocuments.value))
.map(i => ({ .map(i => ({
...i, ...i,
docTotal: useSum().getCreatedDocumentSum(i, createddocuments.value), docTotal: useSum().getCreatedDocumentSum(i, createddocuments.value),
statementTotal: Number(i.statementallocations.reduce((n, {amount}) => n + amount, 0)), statementTotal: Number(i.statementallocations.reduce((n, {amount}) => n + amount, 0)),
openSum: (useSum().getCreatedDocumentSum(i, createddocuments.value) - Number(i.statementallocations.reduce((n, {amount}) => n + amount, 0))).toFixed(2) openSum: useSum().getCreatedDocumentOpenAmount(i, createddocuments.value).toFixed(2)
})) }))
openIncomingInvoices.value = invoiceItems openIncomingInvoices.value = invoiceItems
@@ -504,7 +504,7 @@ onMounted(() => {
placeholder="Konten" placeholder="Konten"
class="w-48" class="w-48"
/> />
<UDivider orientation="vertical" class="h-6"/> <USeparator orientation="vertical" class="h-6"/>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<USelectMenu <USelectMenu
v-model="selectedPeriod" v-model="selectedPeriod"
@@ -601,17 +601,18 @@ onMounted(() => {
</div> </div>
<PageLeaveGuard :when="isSyncing"/> <PageLeaveGuard :when="isSyncing"/>
<UModal v-model="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }"> <UModal v-model:open="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }">
<UCard> <template #content>
<template #header> <UCard>
<div class="flex items-center justify-between gap-3"> <template #header>
<div> <div class="flex items-center justify-between gap-3">
<div class="text-lg font-semibold">Vorschlaege fuer Bankbuchungen</div> <div>
<div class="text-sm text-gray-500">Direkte Zuweisung von passenden Rechnungen und Eingangsbelegen</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> </div>
<UBadge color="primary" variant="subtle">{{ suggestionCount }}</UBadge> </template>
</div>
</template>
<div v-if="rowsWithSuggestions.length > 0" class="grid grid-cols-1 lg:grid-cols-3 gap-4 min-h-[520px]"> <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"> <div class="lg:col-span-1 border rounded-lg overflow-hidden dark:border-gray-800">
@@ -633,7 +634,7 @@ onMounted(() => {
</div> </div>
<div class="mt-2 flex flex-wrap gap-1"> <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?.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> </div>
</div> </div>
@@ -690,7 +691,7 @@ onMounted(() => {
</UBadge> </UBadge>
</div> </div>
</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 Zuweisen
</UButton> </UButton>
<UButton size="sm" color="gray" variant="ghost" @click="dismissSuggestion(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice.suggestionKey, $event)"> <UButton size="sm" color="gray" variant="ghost" @click="dismissSuggestion(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice.suggestionKey, $event)">
@@ -701,10 +702,11 @@ onMounted(() => {
</div> </div>
</div> </div>
<div v-else class="py-10 text-center text-gray-400"> <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"/> <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> <p>Keine Vorschlaege fuer die aktuelle Filterung vorhanden.</p>
</div> </div>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -53,13 +53,13 @@ const setup = async () => {
customers.value = (await useEntities("customers").select()) customers.value = (await useEntities("customers").select())
vendors.value = (await useEntities("vendors").select()) vendors.value = (await useEntities("vendors").select())
openDocuments.value = documents.filter(i => i.statementallocations.reduce((n, {amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i, createddocuments.value).toFixed(2)) openDocuments.value = documents.filter(i => useSum().isOpenCreatedDocument(i, createddocuments.value))
openDocuments.value = openDocuments.value.map(i => { openDocuments.value = openDocuments.value.map(i => {
return { return {
...i, ...i,
docTotal: useSum().getCreatedDocumentSum(i, createddocuments.value), docTotal: useSum().getCreatedDocumentSum(i, createddocuments.value),
statementTotal: Number(i.statementallocations.reduce((n, {amount}) => n + amount, 0)), statementTotal: Number(i.statementallocations.reduce((n, {amount}) => n + amount, 0)),
openSum: (useSum().getCreatedDocumentSum(i, createddocuments.value) - Number(i.statementallocations.reduce((n, {amount}) => n + amount, 0))).toFixed(2) openSum: useSum().getCreatedDocumentOpenAmount(i, createddocuments.value).toFixed(2)
} }
}) })
@@ -438,7 +438,7 @@ setup()
<UBadge v-else color="amber" variant="subtle">Offen</UBadge> <UBadge v-else color="amber" variant="subtle">Offen</UBadge>
</template> </template>
<template #right> <template #right>
<ArchiveButton color="rose" variant="outline" type="bankstatements" @confirmed="archiveStatement"/> <ArchiveButton color="error" variant="outline" type="bankstatements" @confirmed="archiveStatement"/>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
@@ -552,7 +552,7 @@ setup()
<div class="font-mono text-sm font-semibold">{{ displayCurrency(item.amount) }}</div> <div class="font-mono text-sm font-semibold">{{ displayCurrency(item.amount) }}</div>
<UButton <UButton
icon="i-heroicons-trash" icon="i-heroicons-trash"
color="rose" color="error"
variant="ghost" variant="ghost"
size="xs" size="xs"
class="opacity-0 group-hover:opacity-100 transition-opacity" 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"> 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="grid grid-cols-12 gap-4 items-end">
<div class="col-span-12 md:col-span-3"> <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"> <UInput v-model="manualAllocationSum" type="number" step="0.01">
<template #trailing><span class="text-gray-500 text-xs">EUR</span></template> <template #trailing><span class="text-gray-500 text-xs">EUR</span></template>
</UInput> </UInput>
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-12 md:col-span-5"> <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"> <div class="flex gap-1">
<USelectMenu <USelectMenu
class="w-full" class="w-full"
@@ -607,7 +607,7 @@ setup()
/> />
</UTooltip> </UTooltip>
</div> </div>
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-12 md:col-span-4 flex justify-end gap-2 pb-0.5"> <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" <UButton variant="soft" color="gray" icon="i-heroicons-adjustments-horizontal"
@@ -747,7 +747,7 @@ setup()
</div> </div>
<UButton <UButton
size="xs" 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'})" @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 Beleg zuweisen
@@ -867,7 +867,7 @@ setup()
v-if="!itemInfo.statementallocations.find(i => i.incominginvoice === item.id)" v-if="!itemInfo.statementallocations.find(i => i.incominginvoice === item.id)"
icon="i-heroicons-check" icon="i-heroicons-check"
size="sm" size="sm"
color="rose" color="error"
variant="soft" 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})" @click="saveAllocation({incominginvoice: item.id, bankstatement: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(item,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(item,true)), description: allocationDescription})"
/> />

View File

@@ -1,9 +1,9 @@
<script setup> <script setup>
import dayjs from "dayjs" import dayjs from "dayjs"
import Handlebars from "handlebars"
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import {useFunctions} from "~/composables/useFunctions.js"; import {useFunctions} from "~/composables/useFunctions.js";
import EntityModalButtons from "~/components/EntityModalButtons.vue"; import EntityModalButtons from "~/components/EntityModalButtons.vue";
import { documentTemplateHandlebars } from "~/utils/handlebars";
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
@@ -1098,8 +1098,8 @@ const getDocumentData = async () => {
}) })
//Compile Start & EndText //Compile Start & EndText
const templateStartText = Handlebars.compile(itemInfo.value.startText); const templateStartText = documentTemplateHandlebars.compile(itemInfo.value.startText);
const templateEndText = Handlebars.compile(itemInfo.value.endText); const templateEndText = documentTemplateHandlebars.compile(itemInfo.value.endText);
const generateContext = (itemInfo, contactData) => { const generateContext = (itemInfo, contactData) => {
return { return {
@@ -1283,7 +1283,9 @@ const generateDocument = async () => {
} }
const onChangeTab = (index) => { const onChangeTab = (index) => {
if (index === 1) { selectedTab.value = String(index)
if (String(index) === "1") {
generateDocument() generateDocument()
} }
} }
@@ -1441,13 +1443,13 @@ const saveDocument = async (state, resetup = false) => {
if (resetup) await setupPage() if (resetup) await setupPage()
} }
const selectedTab = ref(0) const selectedTab = ref("0")
const closeDocument = async () => { const closeDocument = async () => {
if(selectedTab.value === 0) { if(selectedTab.value === "0") {
await generateDocument() await generateDocument()
selectedTab.value = 1 selectedTab.value = "1"
} else { } else {
loaded.value = false loaded.value = false
@@ -1609,7 +1611,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</template> </template>
<template #right> <template #right>
<ArchiveButton <ArchiveButton
color="rose" color="error"
type="createddocuments" type="createddocuments"
v-if="itemInfo.state === 'Entwurf' || itemInfo.type === 'serialInvoices'" v-if="itemInfo.state === 'Entwurf' || itemInfo.type === 'serialInvoices'"
variant="outline" variant="outline"
@@ -1628,7 +1630,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
@click="closeDocument" @click="closeDocument"
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices'" v-if="itemInfo.id && itemInfo.type !== 'serialInvoices'"
> >
{{selectedTab === 0 ? "Vorschau zeigen" : "Fertigstellen"}} {{selectedTab === '0' ? "Vorschau zeigen" : "Fertigstellen"}}
</UButton> </UButton>
<UButton <UButton
icon="i-mdi-content-save" icon="i-mdi-content-save"
@@ -1640,13 +1642,13 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UDashboardPanelContent> <UDashboardPanelContent>
<UTabs class="p-5" :items="tabItems" @change="onChangeTab" v-if="loaded" v-model="selectedTab"> <UTabs class="p-5" :items="tabItems" @update:model-value="onChangeTab" v-if="loaded" v-model="selectedTab">
<template #item="{item}"> <template #content="{item}">
<div v-if="item.label === 'Editor'"> <div v-if="item.label === 'Editor'">
<UAlert <UAlert
class="my-5" class="my-5"
title="Vorhandene Probleme und Informationen:" title="Vorhandene Probleme und Informationen:"
:color="findDocumentErrors.filter(i => i.type === 'breaking').length > 0 ? 'rose' : 'white'" :color="findDocumentErrors.filter(i => i.type === 'breaking').length > 0 ? 'error' : 'white'"
variant="outline" variant="outline"
v-if="findDocumentErrors.length > 0" v-if="findDocumentErrors.length > 0"
> >
@@ -1664,7 +1666,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<InputGroup> <InputGroup>
<div class="w-1/3 mr-5"> <div class="w-1/3 mr-5">
<UFormGroup <UFormField
label="Dokumenttyp:" label="Dokumenttyp:"
> >
<InputGroup> <InputGroup>
@@ -1689,14 +1691,15 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</InputGroup> </InputGroup>
<USlideover <USlideover
v-model="showAdvanceInvoiceCalcModal" v-model:open="showAdvanceInvoiceCalcModal"
> >
<UCard class="h-full"> <template #body>
<template #header> <UCard class="h-full">
<UButton @click="importPositions">Übernehmen</UButton> <template #header>
</template> <UButton @click="importPositions">Übernehmen</UButton>
</template>
<UFormGroup <UFormField
label="Gesamtsumme:" label="Gesamtsumme:"
> >
<UInput <UInput
@@ -1705,8 +1708,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-model="advanceInvoiceData.totalSumNet" v-model="advanceInvoiceData.totalSumNet"
@focusout="advanceInvoiceData.part = advanceInvoiceData.totalSumNet / 100 * advanceInvoiceData.partPerPecentage" @focusout="advanceInvoiceData.part = advanceInvoiceData.totalSumNet / 100 * advanceInvoiceData.partPerPecentage"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Prozent:" label="Prozent:"
> >
<UInput <UInput
@@ -1715,27 +1718,26 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-model="advanceInvoiceData.partPerPecentage" v-model="advanceInvoiceData.partPerPecentage"
@focusout="advanceInvoiceData.part = advanceInvoiceData.totalSumNet / 100 * advanceInvoiceData.partPerPecentage" @focusout="advanceInvoiceData.part = advanceInvoiceData.totalSumNet / 100 * advanceInvoiceData.partPerPecentage"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Abzurechnender Anteil:" label="Abzurechnender Anteil:"
> >
<UInput <UInput
type="number" type="number"
:step="0.01" :step="0.01"
v-model="advanceInvoiceData.part" v-model="advanceInvoiceData.part"
@focusout="advanceInvoiceData.partPerPecentage = Number((advanceInvoiceData.part / advanceInvoiceData.totalSumNet * 100).toFixed(2))" @focusout="advanceInvoiceData.partPerPecentage = Number((advanceInvoiceData.part / advanceInvoiceData.totalSumNet * 100).toFixed(2))"
/> />
</UFormGroup> </UFormField>
</UCard>
</template>
</UCard>
</USlideover> </USlideover>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Steuertyp:" label="Steuertyp:"
v-if="['invoices','advanceInvoices','quotes','confirmationOrders','serialInvoices'].includes(itemInfo.type)" v-if="['invoices','advanceInvoices','quotes','confirmationOrders','serialInvoices'].includes(itemInfo.type)"
> >
@@ -1747,9 +1749,9 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
@change="setTaxType" @change="setTaxType"
class="w-full" class="w-full"
></USelectMenu> ></USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Briefpapier:" label="Briefpapier:"
> >
<USelectMenu <USelectMenu
@@ -1761,15 +1763,15 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
searchable-placeholder="Suche..." searchable-placeholder="Suche..."
:search-attributes="['name']" :search-attributes="['name']"
class="w-full" class="w-full"
:color="itemInfo.letterhead ? 'primary' : 'rose'" :color="itemInfo.letterhead ? 'primary' : 'error'"
> >
<template #label> <template #label>
{{ itemInfo.letterhead ? letterheads.find(i => i.id === itemInfo.letterhead).name : "Kein Briefpapier gewählt" }} {{ itemInfo.letterhead ? letterheads.find(i => i.id === itemInfo.letterhead).name : "Kein Briefpapier gewählt" }}
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Kunde:" label="Kunde:"
> >
<div class="flex flex-row"> <div class="flex flex-row">
@@ -1788,7 +1790,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
{{ option.name }}{{ option.nameAddition }} {{ option.name }}{{ option.nameAddition }}
</template> </template>
<UButton <UButton
:color="itemInfo.customer ? 'primary' : 'rose'" :color="itemInfo.customer ? 'primary' : 'error'"
variant="outline" variant="outline"
class="w-full" class="w-full"
> >
@@ -1860,8 +1862,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UAlert> </UAlert>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Ansprechpartner:" label="Ansprechpartner:"
v-if="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).isCompany : false " v-if="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).isCompany : false "
> >
@@ -1902,7 +1904,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>Kontakt</UButton>--> >Kontakt</UButton>-->
<UButton <UButton
variant="outline" variant="outline"
color="rose" color="error"
v-if="itemInfo.contact" v-if="itemInfo.contact"
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="itemInfo.contact = null" @click="itemInfo.contact = null"
@@ -1915,15 +1917,15 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
/> />
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Adresse:" label="Adresse:"
> >
<UInput <UInput
v-model="itemInfo.address.street" v-model="itemInfo.address.street"
:placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.street : 'Straße + Hausnummer'" :placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.street : 'Straße + Hausnummer'"
:color="itemInfo.address.street ? 'primary' : 'rose'" :color="itemInfo.address.street ? 'primary' : 'error'"
/> />
<UInput <UInput
v-model="itemInfo.address.special" v-model="itemInfo.address.special"
@@ -1940,19 +1942,19 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
@input="sanitizeAddressZipInput" @input="sanitizeAddressZipInput"
@change="checkAddressZip" @change="checkAddressZip"
:placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.zip : 'PLZ'" :placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.zip : 'PLZ'"
:color="itemInfo.address.zip ? 'primary' : 'rose'" :color="itemInfo.address.zip ? 'primary' : 'error'"
/> />
<UInput <UInput
class="flex-auto" class="flex-auto"
v-model="itemInfo.address.city" v-model="itemInfo.address.city"
:placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.city : 'Ort'" :placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.city : 'Ort'"
:color="itemInfo.address.city ? 'primary' : 'rose'" :color="itemInfo.address.city ? 'primary' : 'error'"
/> />
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
</div> </div>
<div class="w-2/3"> <div class="w-2/3">
<UFormGroup <UFormField
:label="itemInfo.documentNumberTitle + ':'" :label="itemInfo.documentNumberTitle + ':'"
v-if="itemInfo.type !== 'serialInvoices'" v-if="itemInfo.type !== 'serialInvoices'"
> >
@@ -1961,10 +1963,10 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
placeholder="XXXX" placeholder="XXXX"
disabled disabled
/> />
</UFormGroup> </UFormField>
<InputGroup class="w-full"> <InputGroup class="w-full">
<UFormGroup <UFormField
class="w-80 mr-1" class="w-80 mr-1"
label="Lieferdatumsart:" label="Lieferdatumsart:"
v-if="itemInfo.type !== 'serialInvoices'" v-if="itemInfo.type !== 'serialInvoices'"
@@ -1975,8 +1977,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
:label="`${itemInfo.deliveryDateType}${['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.deliveryDateType) ? ' Start' : ''}:`" :label="`${itemInfo.deliveryDateType}${['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.deliveryDateType) ? ' Start' : ''}:`"
v-if="itemInfo.type !== 'serialInvoices' && itemInfo.deliveryDateType !== 'Kein Lieferdatum anzeigen'" v-if="itemInfo.type !== 'serialInvoices' && itemInfo.deliveryDateType !== 'Kein Lieferdatum anzeigen'"
class="mr-1" class="mr-1"
@@ -1994,8 +1996,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
:label="itemInfo.deliveryDateType + ' Ende:'" :label="itemInfo.deliveryDateType + ' Ende:'"
v-if="itemInfo.type !== 'serialInvoices' && ['Lieferzeitraum','Leistungszeitraum'].includes(itemInfo.deliveryDateType)" v-if="itemInfo.type !== 'serialInvoices' && ['Lieferzeitraum','Leistungszeitraum'].includes(itemInfo.deliveryDateType)"
> >
@@ -2012,11 +2014,11 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<LazyDatePicker v-model="itemInfo.deliveryDateEnd" @close="close"/> <LazyDatePicker v-model="itemInfo.deliveryDateEnd" @close="close"/>
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
</InputGroup> </InputGroup>
<InputGroup class="w-full"> <InputGroup class="w-full">
<UFormGroup <UFormField
label="Belegdatum:" label="Belegdatum:"
class="mr-1" class="mr-1"
v-if="itemInfo.type !== 'serialInvoices'" v-if="itemInfo.type !== 'serialInvoices'"
@@ -2032,10 +2034,10 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<LazyDatePicker v-model="itemInfo.documentDate" @close="close"/> <LazyDatePicker v-model="itemInfo.documentDate" @close="close"/>
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
class="w-full" class="w-full"
label="Zahlungsziel in Tagen:" label="Zahlungsziel in Tagen:"
> >
@@ -2043,8 +2045,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
type="number" type="number"
v-model="itemInfo.paymentDays" v-model="itemInfo.paymentDays"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
class="w-full" class="w-full"
label="Zahlungsart:" label="Zahlungsart:"
> >
@@ -2066,8 +2068,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
{{itemInfo.payment_type === 'transfer' ? "Überweisung" : "SEPA-Lastschrift"}} {{itemInfo.payment_type === 'transfer' ? "Überweisung" : "SEPA-Lastschrift"}}
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
class="w-full" class="w-full"
label="Individueller Aufschlag:" label="Individueller Aufschlag:"
> >
@@ -2081,9 +2083,9 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span> <span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
</InputGroup> </InputGroup>
<UFormGroup <UFormField
label="Mitarbeiter:" label="Mitarbeiter:"
> >
<USelectMenu <USelectMenu
@@ -2093,22 +2095,22 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
value-attribute="id" value-attribute="id"
@change="setContactPersonData" @change="setContactPersonData"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Kontakt Telefon:" label="Kontakt Telefon:"
> >
<UInput <UInput
v-model="itemInfo.contactTel" v-model="itemInfo.contactTel"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Kontakt E-Mail:" label="Kontakt E-Mail:"
> >
<UInput <UInput
v-model="itemInfo.contactEMail" v-model="itemInfo.contactEMail"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Objekt:" label="Objekt:"
> >
<InputGroup> <InputGroup>
@@ -2132,7 +2134,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</USelectMenu> </USelectMenu>
<UButton <UButton
variant="outline" variant="outline"
color="rose" color="error"
v-if="itemInfo.plant" v-if="itemInfo.plant"
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="itemInfo.plant = null" @click="itemInfo.plant = null"
@@ -2145,8 +2147,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
/> />
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Projekt:" label="Projekt:"
> >
<InputGroup> <InputGroup>
@@ -2171,7 +2173,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</USelectMenu> </USelectMenu>
<UButton <UButton
variant="outline" variant="outline"
color="rose" color="error"
v-if="itemInfo.project" v-if="itemInfo.project"
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="itemInfo.project = null" @click="itemInfo.project = null"
@@ -2184,8 +2186,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
/> />
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Vertrag:" label="Vertrag:"
> >
<InputGroup> <InputGroup>
@@ -2210,7 +2212,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</USelectMenu> </USelectMenu>
<UButton <UButton
variant="outline" variant="outline"
color="rose" color="error"
v-if="itemInfo.contract" v-if="itemInfo.contract"
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="itemInfo.contract = null" @click="itemInfo.contract = null"
@@ -2223,19 +2225,19 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
/> />
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
</div> </div>
</InputGroup> </InputGroup>
<div v-if="itemInfo.type === 'serialInvoices'" class="mb-5"> <div v-if="itemInfo.type === 'serialInvoices'" class="mb-5">
<UDivider class="mt-5 mb-3"> <USeparator class="mt-5 mb-3">
Einstellungen für die Serienrechnung Einstellungen für die Serienrechnung
</UDivider> </USeparator>
<div class="flex flex-row"> <div class="flex flex-row">
<div class="w-1/3"> <div class="w-1/3">
<UFormGroup <UFormField
label="Datum erste Ausführung:" label="Datum erste Ausführung:"
> >
<UPopover :popper="{ placement: 'bottom-start' }"> <UPopover :popper="{ placement: 'bottom-start' }">
@@ -2249,8 +2251,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<LazyDatePicker v-model="itemInfo.serialConfig.firstExecution" @close="close"/> <LazyDatePicker v-model="itemInfo.serialConfig.firstExecution" @close="close"/>
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Datum letzte Ausführung:" label="Datum letzte Ausführung:"
> >
<UPopover :popper="{ placement: 'bottom-start' }"> <UPopover :popper="{ placement: 'bottom-start' }">
@@ -2264,7 +2266,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<LazyDatePicker v-model="itemInfo.serialConfig.executionUntil" @close="close"/> <LazyDatePicker v-model="itemInfo.serialConfig.executionUntil" @close="close"/>
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
<UCheckbox <UCheckbox
v-model="itemInfo.serialConfig.active" v-model="itemInfo.serialConfig.active"
label="Aktiv" label="Aktiv"
@@ -2273,22 +2275,22 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</div> </div>
<div class="w-2/3"> <div class="w-2/3">
<UFormGroup <UFormField
label="Intervall:" label="Intervall:"
> >
<USelectMenu <USelectMenu
v-model="itemInfo.serialConfig.intervall" v-model="itemInfo.serialConfig.intervall"
:options="['wöchentlich','2 - wöchentlich', 'monatlich', 'vierteljährlich','halbjährlich', 'jährlich']" :options="['wöchentlich','2 - wöchentlich', 'monatlich', 'vierteljährlich','halbjährlich', 'jährlich']"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Richtung:" label="Richtung:"
> >
<USelectMenu <USelectMenu
v-model="itemInfo.serialConfig.dateDirection" v-model="itemInfo.serialConfig.dateDirection"
:options="['Rückwirkend','Im Voraus']" :options="['Rückwirkend','Im Voraus']"
/> />
</UFormGroup> </UFormField>
<UAlert <UAlert
title="Anfangs- und Enddatum" title="Anfangs- und Enddatum"
description="Für das Anfangs- und Enddatum werden jeweils der ersten und letzte Tag des ausgewählten Intervalls und der Richtung automatisch ausgewählt" description="Für das Anfangs- und Enddatum werden jeweils der ersten und letzte Tag des ausgewählten Intervalls und der Richtung automatisch ausgewählt"
@@ -2301,27 +2303,27 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</div> </div>
<UDivider <USeparator
class="my-3" class="my-3"
/> />
<UFormGroup <UFormField
label="Titel:" label="Titel:"
> >
<UInput v-model="itemInfo.title" disabled/> <UInput v-model="itemInfo.title" disabled/>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Beschreibung:" label="Beschreibung:"
class="mt-3" class="mt-3"
> >
<UInput v-model="itemInfo.description"/> <UInput v-model="itemInfo.description"/>
</UFormGroup> </UFormField>
<UDivider <USeparator
class="my-3" class="my-3"
/> />
<UFormGroup <UFormField
label="Vorlage auswählen" label="Vorlage auswählen"
> >
<USelectMenu <USelectMenu
@@ -2337,20 +2339,20 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
{{ texttemplates.find(i => i.text === itemInfo.startText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)) ? texttemplates.find(i => i.text === itemInfo.startText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)).name : "Keine Vorlage ausgewählt oder Vorlage verändert" }} {{ texttemplates.find(i => i.text === itemInfo.startText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)) ? texttemplates.find(i => i.text === itemInfo.startText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)).name : "Keine Vorlage ausgewählt oder Vorlage verändert" }}
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Einleitung:" label="Einleitung:"
> >
<UTextarea <UTextarea
v-model="itemInfo.startText" v-model="itemInfo.startText"
:rows="6" :rows="6"
/> />
</UFormGroup> </UFormField>
<UDivider <USeparator
class="my-3" class="my-3"
/> />
@@ -2389,7 +2391,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-if="row.mode === 'pagebreak'" v-if="row.mode === 'pagebreak'"
colspan="7" colspan="7"
> >
<UDivider/> <USeparator/>
</td> </td>
<td <td
v-if="row.mode === 'text'" v-if="row.mode === 'text'"
@@ -2429,7 +2431,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
:disabled="itemInfo.type === 'cancellationInvoices'" :disabled="itemInfo.type === 'cancellationInvoices'"
class="w-60" class="w-60"
:options="products" :options="products"
:color="row.product ? 'primary' : 'rose'" :color="row.product ? 'primary' : 'error'"
option-attribute="name" option-attribute="name"
value-attribute="id" value-attribute="id"
searchable searchable
@@ -2453,14 +2455,14 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
icon="i-heroicons-magnifying-glass" icon="i-heroicons-magnifying-glass"
@click="showProductSelectionModal = true" @click="showProductSelectionModal = true"
/> />
<UModal v-model="showProductSelectionModal"> <UModal v-model:open="showProductSelectionModal">
<UCard> <UCard>
<template #header> <template #header>
Artikel Auswählen Artikel Auswählen
</template> </template>
<InputGroup class="w-full"> <InputGroup class="w-full">
<UFormGroup label="Artikelkategorie:"> <UFormField label="Artikelkategorie:">
<USelectMenu <USelectMenu
v-if="productcategories.length > 0" v-if="productcategories.length > 0"
:options="[{name: 'Nicht zugeordnet',id:'not set'},...productcategories]" :options="[{name: 'Nicht zugeordnet',id:'not set'},...productcategories]"
@@ -2468,15 +2470,15 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
option-attribute="name" option-attribute="name"
v-model="selectedProductcategorie" v-model="selectedProductcategorie"
/> />
</UFormGroup> </UFormField>
</InputGroup> </InputGroup>
<UTable <UTable
:rows="selectedProductcategorie !== 'not set' ? products.filter(i => i.productcategories.includes(selectedProductcategorie)) : products.filter(i => i.productcategories.length === 0)" :rows="selectedProductcategorie !== 'not set' ? products.filter(i => i.productcategories.includes(selectedProductcategorie)) : products.filter(i => i.productcategories.length === 0)"
:columns="[ :columns="normalizeTableColumns([
{key: 'name',label:'Name'}, {key: 'name',label:'Name'},
{key: 'manufacturer',label:'Hersteller'}, {key: 'manufacturer',label:'Hersteller'},
{key: 'articleNumber',label:'Artikelnummer'}, {key: 'articleNumber',label:'Artikelnummer'},
]" ])"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Artikel anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Artikel anzuzeigen' }"
@select=" (i) => { @select=" (i) => {
row.product = i.id row.product = i.id
@@ -2501,7 +2503,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
:disabled="itemInfo.type === 'cancellationInvoices'" :disabled="itemInfo.type === 'cancellationInvoices'"
class="w-60" class="w-60"
:options="services" :options="services"
:color="row.service ? 'primary' : 'rose'" :color="row.service ? 'primary' : 'error'"
option-attribute="name" option-attribute="name"
value-attribute="id" value-attribute="id"
searchable searchable
@@ -2525,14 +2527,14 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
icon="i-heroicons-magnifying-glass" icon="i-heroicons-magnifying-glass"
@click="showServiceSelectionModal = true" @click="showServiceSelectionModal = true"
/> />
<UModal v-model="showServiceSelectionModal"> <UModal v-model:open="showServiceSelectionModal">
<UCard> <UCard>
<template #header> <template #header>
Leistung Auswählen Leistung Auswählen
</template> </template>
<InputGroup class="w-full"> <InputGroup class="w-full">
<UFormGroup label="Leistungskategorie:"> <UFormField label="Leistungskategorie:">
<USelectMenu <USelectMenu
v-if="servicecategories.length > 0" v-if="servicecategories.length > 0"
:options="[{name: 'Nicht zugeordnet',id:'not set'},...servicecategories]" :options="[{name: 'Nicht zugeordnet',id:'not set'},...servicecategories]"
@@ -2540,15 +2542,15 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
option-attribute="name" option-attribute="name"
v-model="selectedServicecategorie" v-model="selectedServicecategorie"
/> />
</UFormGroup> </UFormField>
</InputGroup> </InputGroup>
<UTable <UTable
:rows="selectedServicecategorie !== 'not set' ? services.filter(i => i.servicecategories.includes(selectedServicecategorie)) : services.filter(i => i.servicecategories.length === 0)" :rows="selectedServicecategorie !== 'not set' ? services.filter(i => i.servicecategories.includes(selectedServicecategorie)) : services.filter(i => i.servicecategories.length === 0)"
:columns="[ :columns="normalizeTableColumns([
{key: 'name',label:'Name'}, {key: 'name',label:'Name'},
{key: 'serviceNumber',label:'Leistungsnummer'}, {key: 'serviceNumber',label:'Leistungsnummer'},
{key: 'sellingPrice',label:'Verkaufspreis'}, {key: 'sellingPrice',label:'Verkaufspreis'},
]" ])"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Leistungen anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Leistungen anzuzeigen' }"
@select=" (i) => { @select=" (i) => {
row.service = i.id row.service = i.id
@@ -2670,8 +2672,9 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-if="row.agriculture" v-if="row.agriculture"
@click="row.showEditDiesel = true" @click="row.showEditDiesel = true"
/> />
<UModal v-model="row.showEdit"> <UModal v-model:open="row.showEdit">
<UCard> <template #content>
<UCard>
<!-- <template #header> <!-- <template #header>
Zeile bearbeiten Zeile bearbeiten
</template>--> </template>-->
@@ -2685,7 +2688,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</div> </div>
</template> </template>
<InputGroup> <InputGroup>
<UFormGroup <UFormField
label="Anzahl:" label="Anzahl:"
class="flex-auto" class="flex-auto"
> >
@@ -2695,8 +2698,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
:step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : '1' " :step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : '1' "
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Einheit:" label="Einheit:"
class="flex-auto" class="flex-auto"
> >
@@ -2711,10 +2714,10 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
{{ units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).name : "Keine Einheit gewählt" }} {{ units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).name : "Keine Einheit gewählt" }}
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
</InputGroup> </InputGroup>
<UFormGroup <UFormField
label="Einzelpreis:" label="Einzelpreis:"
v-if="itemInfo.type !== 'deliveryNotes'" v-if="itemInfo.type !== 'deliveryNotes'"
> >
@@ -2743,8 +2746,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
class="text-gray-500 dark:text-gray-400 text-xs"> </span> class="text-gray-500 dark:text-gray-400 text-xs"> </span>
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Umsatzsteuer:" label="Umsatzsteuer:"
class="mt-3" class="mt-3"
v-if="itemInfo.type !== 'deliveryNotes'" v-if="itemInfo.type !== 'deliveryNotes'"
@@ -2761,9 +2764,9 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
{{ row.taxPercent }} % {{ row.taxPercent }} %
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Rabatt:" label="Rabatt:"
class="mt-3" class="mt-3"
v-if="itemInfo.type !== 'deliveryNotes'" v-if="itemInfo.type !== 'deliveryNotes'"
@@ -2778,25 +2781,25 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span> <span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
<InputGroup class="w-full mt-3"> <InputGroup class="w-full mt-3">
<UFormGroup <UFormField
label="Optional:" label="Optional:"
> >
<UToggle <USwitch
:disabled="row.alternative" :disabled="row.alternative"
v-model="row.optional" v-model="row.optional"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
class="ml-3" class="ml-3"
label="Alternativ:" label="Alternativ:"
> >
<UToggle <USwitch
:disabled="row.optional" :disabled="row.optional"
v-model="row.alternative" v-model="row.alternative"
/> />
</UFormGroup> </UFormField>
</InputGroup> </InputGroup>
<UAlert <UAlert
@@ -2819,7 +2822,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</li> </li>
</ul> </ul>
<UFormGroup <UFormField
label="Beschreibung:" label="Beschreibung:"
class="mt-3" class="mt-3"
> >
@@ -2829,7 +2832,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
> >
</UTextarea> </UTextarea>
</UFormGroup> </UFormField>
<!-- <template #footer> <!-- <template #footer>
<UButton <UButton
@@ -2838,16 +2841,16 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
Speichern Speichern
</UButton> </UButton>
</template>--> </template>-->
</UCard> </UCard>
</template>
</UModal> </UModal>
<UModal v-model="row.showEditDiesel"> <UModal v-model:open="row.showEditDiesel">
<UCard> <template #content>
<template #header> <UCard>
Dieselverbrauch bearbeiten <template #header>
</template> Dieselverbrauch bearbeiten
<UFormGroup </template>
<UFormField
label="Menge Diesel:" label="Menge Diesel:"
> >
<UInput <UInput
@@ -2861,8 +2864,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
L L
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Preis Diesel:" label="Preis Diesel:"
> >
<UInput <UInput
@@ -2875,8 +2878,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
€/L €/L
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Menge AdBlue:" label="Menge AdBlue:"
> >
<UInput <UInput
@@ -2888,8 +2891,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
L L
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Preis AdBlue:" label="Preis AdBlue:"
> >
<UInput <UInput
@@ -2901,18 +2904,17 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
€/L €/L
</template> </template>
</UInput> </UInput>
</UFormGroup> </UFormField>
<template #footer> <template #footer>
<UButton <UButton
@click="row.showEditDiesel = false, @click="row.showEditDiesel = false,
processDieselPosition()" processDieselPosition()"
> >
Speichern Speichern
</UButton> </UButton>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</td> </td>
<td <td
@@ -2942,7 +2944,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UButton <UButton
:disabled="itemInfo.type === 'cancellationInvoices'" :disabled="itemInfo.type === 'cancellationInvoices'"
variant="ghost" variant="ghost"
color="rose" color="error"
icon="i-heroicons-x-mark-16-solid" icon="i-heroicons-x-mark-16-solid"
@click="removePosition(row.id)" @click="removePosition(row.id)"
/> />
@@ -2954,7 +2956,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UAlert <UAlert
v-else v-else
title="Keine Positionen hinzugefügt" title="Keine Positionen hinzugefügt"
color="rose" color="error"
variant="outline" variant="outline"
icon="i-heroicons-light-bulb" icon="i-heroicons-light-bulb"
></UAlert> ></UAlert>
@@ -3004,12 +3006,12 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UButton> </UButton>
</InputGroup> </InputGroup>
<!-- <UDivider <!-- <USeparator
class="mt-5 mb-3" class="mt-5 mb-3"
v-if="openAdvanceInvoices.length > 0 || itemInfo.usedAdvanceInvoices.length > 0" v-if="openAdvanceInvoices.length > 0 || itemInfo.usedAdvanceInvoices.length > 0"
> >
Noch nicht abgerechnete Abschlagsrechnungen Noch nicht abgerechnete Abschlagsrechnungen
</UDivider> </USeparator>
<div <div
v-for="advanceInvoice in openAdvanceInvoices" v-for="advanceInvoice in openAdvanceInvoices"
@@ -3029,7 +3031,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UButton <UButton
@click="itemInfo.usedAdvanceInvoices = itemInfo.usedAdvanceInvoices.filter(i => i !== advanceInvoice.id)" @click="itemInfo.usedAdvanceInvoices = itemInfo.usedAdvanceInvoices.filter(i => i !== advanceInvoice.id)"
:disabled="!itemInfo.usedAdvanceInvoices.includes(advanceInvoice.id)" :disabled="!itemInfo.usedAdvanceInvoices.includes(advanceInvoice.id)"
color="rose" color="error"
variant="outline" variant="outline"
> >
X X
@@ -3037,7 +3039,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</InputGroup> </InputGroup>
</div>--> </div>-->
<UDivider class="my-3" v-if="Object.keys(documentTotal.titleSums).length > 0">Überschriften</UDivider> <USeparator class="my-3" v-if="Object.keys(documentTotal.titleSums).length > 0" label="Überschriften"/>
<table> <table>
<tr v-for="sumKey in Object.keys(documentTotal.titleSums) "> <tr v-for="sumKey in Object.keys(documentTotal.titleSums) ">
@@ -3046,7 +3048,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</tr> </tr>
</table> </table>
<UDivider class="my-3" v-if="itemInfo.rows.length > 0">Auswertung & Gesamt</UDivider> <USeparator class="my-3" v-if="itemInfo.rows.length > 0" label="Auswertung & Gesamt"/>
<div class="w-full flex justify-between" v-if="itemInfo.type !== 'deliveryNotes'"> <div class="w-full flex justify-between" v-if="itemInfo.type !== 'deliveryNotes'">
@@ -3112,9 +3114,9 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</table> </table>
</div> </div>
<!-- <UDivider <!-- <USeparator
class="my-3" class="my-3"
>Auswertung</UDivider> >Auswertung</USeparator>
<div class="w-full flex justify-end"> <div class="w-full flex justify-end">
<table class="w-1/3"> <table class="w-1/3">
@@ -3151,11 +3153,11 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UDivider <USeparator
class="my-3" class="my-3"
/> />
<UFormGroup <UFormField
label="Vorlage auswählen" label="Vorlage auswählen"
> >
<USelectMenu <USelectMenu
@@ -3171,16 +3173,16 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
{{texttemplates.find(i => i.text === itemInfo.endText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)) ? texttemplates.find(i => i.text === itemInfo.endText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)).name : "Keine Vorlage ausgewählt oder Vorlage verändert"}} {{texttemplates.find(i => i.text === itemInfo.endText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)) ? texttemplates.find(i => i.text === itemInfo.endText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)).name : "Keine Vorlage ausgewählt oder Vorlage verändert"}}
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Nachbemerkung:" label="Nachbemerkung:"
> >
<UTextarea <UTextarea
v-model="itemInfo.endText" v-model="itemInfo.endText"
:rows="6" :rows="6"
/> />
</UFormGroup> </UFormField>
</div> </div>
<div v-else-if="item.label === 'Vorschau'"> <div v-else-if="item.label === 'Vorschau'">
<PDFViewer <PDFViewer

View File

@@ -16,7 +16,7 @@
</UInput> </UInput>
<UButton <UButton
v-if="searchString.length > 0" v-if="searchString.length > 0"
color="rose" color="error"
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
variant="outline" variant="outline"
@click="clearSearchString()" @click="clearSearchString()"
@@ -55,82 +55,82 @@
{{ getRowsForTab(item.key).length }} {{ getRowsForTab(item.key).length }}
</UBadge> </UBadge>
</template> </template>
<template #item="{item}"> <template #content="{item}">
<div style="height: 80vh; overflow-y: scroll"> <div style="height: 80vh; overflow-y: scroll">
<UTable <UTable
:columns="getColumnsForTab(item.key)" :columns="normalizeTableColumns(getColumnsForTab(item.key))"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
:rows="getRowsForTab(item.key)" :data="getRowsForTab(item.key)"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
class="w-full" class="w-full"
@select="selectItem" :on-select="selectItem"
> >
<template #type-data="{row}"> <template #type-cell="{row}">
<span v-if="row.type === 'cancellationInvoices'" class="text-cyan-500">{{ <span v-if="row.original.type === 'cancellationInvoices'" class="text-cyan-500">{{
dataStore.documentTypesForCreation[row.type].labelSingle dataStore.documentTypesForCreation[row.original.type].labelSingle
}} für {{ filteredRows.find(i => row.createddocument?.id === i.id)?.documentNumber }}</span> }} für {{ filteredRows.find(i => row.original.createddocument?.id === i.id)?.documentNumber }}</span>
<span v-else>{{ dataStore.documentTypesForCreation[row.type].labelSingle }}</span> <span v-else>{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}</span>
</template> </template>
<template #state-data="{row}"> <template #state-cell="{row}">
<span v-if="row.state === 'Entwurf'" class="text-rose-500">{{ row.state }}</span> <span v-if="row.original.state === 'Entwurf'" class="text-rose-500">{{ row.original.state }}</span>
<span <span
v-if="row.state === 'Gebucht' && !items.find(i => i.createddocument && i.createddocument.id === row.id)" v-if="row.original.state === 'Gebucht' && !hasCancellationInvoice(row.original)"
class="text-primary-500" class="text-primary-500"
> >
{{ row.state }} {{ row.original.state }}
</span> </span>
<span <span
v-else-if="row.state === 'Gebucht' && items.find(i => i.createddocument && i.createddocument.id === row.id && i.type === 'cancellationInvoices') && ['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" class="text-cyan-500"
> >
Storniert mit {{ items.find(i => i.createddocument && i.createddocument.id === row.id).documentNumber }} Storniert mit {{ getCancellationInvoice(row.original)?.documentNumber }}
</span> </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>
<template #partner-data="{row}"> <template #partner-cell="{row}">
<span v-if="row.customer && row.customer.name.length < 21">{{ row.customer ? row.customer.name : "" }}</span> <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.customer && row.customer.name.length > 20" :text="row.customer.name"> <UTooltip v-else-if="row.original.customer && row.original.customer.name.length > 20" :text="row.original.customer.name">
{{ row.customer.name.substring(0, 20) }}... {{ row.original.customer.name.substring(0, 20) }}...
</UTooltip> </UTooltip>
</template> </template>
<template #reference-data="{row}"> <template #reference-cell="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{ row.documentNumber }}</span> <span v-if="row.original === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{ row.original.documentNumber }}</span>
<span v-else>{{ row.documentNumber }}</span> <span v-else>{{ row.original.documentNumber }}</span>
</template> </template>
<template #date-data="{row}"> <template #date-cell="{row}">
<span v-if="row.date">{{ row.date ? dayjs(row.date).format("DD.MM.YY") : '' }}</span> <span v-if="row.original.date">{{ row.original.date ? dayjs(row.original.date).format("DD.MM.YY") : '' }}</span>
<span v-if="row.documentDate">{{ row.documentDate ? dayjs(row.documentDate).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>
<template #dueDate-data="{row}"> <template #dueDate-cell="{row}">
<span <span
v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)" v-if="row.original.state === 'Gebucht' && row.original.paymentDays && ['invoices','advanceInvoices'].includes(row.original.type) && !hasCancellationInvoice(row.original)"
:class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' " :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> </span>
</template> </template>
<template #paid-data="{row}"> <template #paid-cell="{row}">
<div <div
v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)"> v-if="(row.original.type === 'invoices' ||row.original.type === 'advanceInvoices') && row.original.state === 'Gebucht' && !hasCancellationInvoice(row.original)">
<span v-if="useSum().getIsPaid(row,items)" class="text-primary-500">Bezahlt</span> <span v-if="useSum().getIsPaid(row.original,items)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span> <span v-else class="text-rose-600">Offen</span>
</div> </div>
</template> </template>
<template #amount-data="{row}"> <template #amount-cell="{row}">
<span v-if="row.type !== 'deliveryNotes'">{{ displayCurrency(useSum().getCreatedDocumentSum(row, items)) }}</span> <span v-if="row.original.type !== 'deliveryNotes'">{{ displayCurrency(useSum().getCreatedDocumentSum(row.original, items)) }}</span>
</template> </template>
<template #amountOpen-data="{row}"> <template #amountOpen-cell="{row}">
<span <span
v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.type) && row.state !== 'Entwurf' && !useSum().getIsPaid(row,items) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id) "> v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.original.type) && row.original.state !== 'Entwurf' && !hasCancellationInvoice(row.original) && !useSum().getIsPaid(row.original,items) ">
{{ displayCurrency(useSum().getCreatedDocumentSum(row, items) - row.statementallocations.reduce((n, {amount}) => n + amount, 0)) }} {{ displayCurrency(useSum().getCreatedDocumentOpenAmount(row.original, items)) }}
</span> </span>
</template> </template>
</UTable> </UTable>
@@ -264,13 +264,22 @@ const clearSearchString = () => {
debouncedSearchString.value = '' debouncedSearchString.value = ''
} }
const getCancellationInvoice = (row) => {
return items.value.find((item) => {
const linkedDocumentId = item.createddocument?.id || item.createddocument
return item.type === 'cancellationInvoices'
&& item.state !== 'Entwurf'
&& !item.archived
&& linkedDocumentId === row.id
})
}
const hasCancellationInvoice = (row) => Boolean(getCancellationInvoice(row))
const openUnpaidInvoicesFilter = { const openUnpaidInvoicesFilter = {
name: 'Nur offene Rechnungen', name: 'Nur offene Rechnungen',
filterFunction: (row) => { filterFunction: (row) => {
return ['invoices', 'advanceInvoices'].includes(row.type) return useSum().isOpenCreatedDocument(row, items.value)
&& row.state === 'Gebucht'
&& !useSum().getIsPaid(row, items.value)
&& !items.value.find(i => i.linkedDocument && i.linkedDocument.id === row.id)
} }
} }
@@ -319,8 +328,6 @@ const getRowsForTab = (tabKey) => {
} }
const isPaid = (item) => { const isPaid = (item) => {
let amountPaid = 0 return useSum().getIsPaid(item, items.value)
item.statementallocations.forEach(allocation => amountPaid += allocation.amount)
return Number(amountPaid.toFixed(2)) === useSum().getCreatedDocumentSum(item, items.value)
} }
</script> </script>

View File

@@ -44,15 +44,15 @@
<USelectMenu <USelectMenu
v-model="selectedFilters" v-model="selectedFilters"
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
:options="filterOptions" :items="filterOptions"
option-attribute="name" label-key="name"
value-attribute="name" value-key="name"
multiple multiple
class="hidden lg:block" class="hidden lg:block"
:color="selectedFilters.length > 0 ? 'primary' : 'white'" :color="selectedFilters.length > 0 ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }" :content="{ width: 'min-w-max' }"
> >
<template #label> <template #default>
Filter Filter
</template> </template>
</USelectMenu> </USelectMenu>
@@ -93,16 +93,16 @@
</div> </div>
<UTable <UTable
:rows="filteredRows" :data="filteredRows"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(row) => router.push(`/createDocument/edit/${row.id}`)" :on-select="(row) => router.push(`/createDocument/edit/${row.id}`)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
> >
<template #actions-data="{ row }"> <template #actions-cell="{ row }">
<div @click.stop> <div @click.stop>
<UDropdown :items="getActionItems(row)" :popper="{ placement: 'bottom-end' }"> <UDropdown :items="getActionItems(row.original)" :popper="{ placement: 'bottom-end' }">
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
@@ -112,53 +112,54 @@
</div> </div>
</template> </template>
<template #type-data="{row}"> <template #type-cell="{row}">
{{dataStore.documentTypesForCreation[row.type].labelSingle}} {{dataStore.documentTypesForCreation[row.original.type].labelSingle}}
</template> </template>
<template #partner-data="{row}"> <template #partner-cell="{row}">
<span v-if="row.customer">{{row.customer ? row.customer.name : ""}}</span> <span v-if="row.original.customer">{{row.original.customer ? row.original.customer.name : ""}}</span>
</template> </template>
<template #amount-data="{row}"> <template #amount-cell="{row}">
{{displayCurrency(calculateDocSum(row))}} {{displayCurrency(calculateDocSum(row.original))}}
</template> </template>
<template #serialConfig.active-data="{row}"> <template #serialConfig.active-cell="{row}">
<span v-if="row.serialConfig.active" class="text-primary">Ja</span> <span v-if="row.original.serialConfig.active" class="text-primary">Ja</span>
<span v-else class="text-rose-600">Nein</span> <span v-else class="text-rose-600">Nein</span>
</template> </template>
<template #contract-data="{row}"> <template #contract-cell="{row}">
<span v-if="row.contract">{{row.contract.contractNumber}} - {{row.contract.name}}</span> <span v-if="row.original.contract">{{row.original.contract.contractNumber}} - {{row.original.contract.name}}</span>
</template> </template>
<template #serialConfig.intervall-data="{row}"> <template #serialConfig.intervall-cell="{row}">
<span v-if="row.serialConfig?.intervall === 'monatlich'">Monatlich</span> <span v-if="row.original.serialConfig?.intervall === 'monatlich'">Monatlich</span>
<span v-if="row.serialConfig?.intervall === 'vierteljährlich'">Quartalsweise</span> <span v-if="row.original.serialConfig?.intervall === 'vierteljährlich'">Quartalsweise</span>
</template> </template>
<template #payment_type-data="{row}"> <template #payment_type-cell="{row}">
<span v-if="row.payment_type === 'transfer'">Überweisung</span> <span v-if="row.original.payment_type === 'transfer'">Überweisung</span>
<span v-else-if="row.payment_type === 'direct-debit'">SEPA - Einzug</span> <span v-else-if="row.original.payment_type === 'direct-debit'">SEPA - Einzug</span>
</template> </template>
</UTable> </UTable>
<UModal v-model="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }"> <UModal v-model:open="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }">
<UCard> <template #content>
<template #header> <UCard>
<div class="flex items-center justify-between"> <template #header>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <div class="flex items-center justify-between">
Serienrechnungen manuell ausführen <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
</h3> Serienrechnungen manuell ausführen
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionModal = false" /> </h3>
</div> <UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionModal = false" />
</template> </div>
</template>
<div class="space-y-4"> <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"> <div class="flex items-center gap-2">
<UInput type="date" v-model="executionDate" class="flex-1" /> <UInput type="date" v-model="executionDate" class="flex-1" />
<UButton color="gray" variant="soft" label="Heute" @click="setExecutionDateToToday" /> <UButton color="gray" variant="soft" label="Heute" @click="setExecutionDateToToday" />
</div> </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 sm:items-center justify-between gap-3">
<div class="flex flex-col sm:flex-row gap-2 w-full sm:w-auto"> <div class="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
@@ -172,9 +173,9 @@
<USelectMenu <USelectMenu
v-model="selectedExecutionIntervall" v-model="selectedExecutionIntervall"
:options="executionIntervallOptions" :items="executionIntervallOptions"
option-attribute="label" label-key="label"
value-attribute="value" value-key="value"
size="sm" size="sm"
class="w-full sm:w-52" 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"> <div class="max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-800 rounded-md">
<UTable <UTable
v-model="selectedExecutionRows" v-model="selectedExecutionRows"
:rows="filteredExecutionList" :data="filteredExecutionList"
:columns="executionColumns" :columns="normalizeTableColumns(executionColumns)"
:ui="{ th: { base: 'whitespace-nowrap' } }" :ui="{ th: { base: 'whitespace-nowrap' } }"
> >
<template #partner-data="{row}"> <template #partner-cell="{row}">
{{row.customer ? row.customer.name : "-"}} {{row.original.customer ? row.original.customer.name : "-"}}
</template> </template>
<template #amount-data="{row}"> <template #amount-cell="{row}">
{{displayCurrency(calculateDocSum(row))}} {{displayCurrency(calculateDocSum(row.original))}}
</template> </template>
<template #serialConfig.intervall-data="{row}"> <template #serialConfig.intervall-cell="{row}">
{{ getIntervallLabel(row.serialConfig?.intervall) }} {{ getIntervallLabel(row.original.serialConfig?.intervall) }}
</template> </template>
<template #contract-data="{row}"> <template #contract-cell="{row}">
{{row.contract?.contractNumber}} - {{row.contract?.name}} {{row.original.contract?.contractNumber}} - {{row.original.contract?.name}}
</template> </template>
<template #plant-data="{row}"> <template #plant-cell="{row}">
{{ row.plant?.name || "-" }} {{ row.original.plant?.name || "-" }}
</template> </template>
</UTable> </UTable>
</div> </div>
@@ -231,58 +232,61 @@
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<UButton color="white" @click="showExecutionModal = false">Abbrechen</UButton> <UButton color="white" @click="showExecutionModal = false">Abbrechen</UButton>
<UButton <UButton
color="primary" color="primary"
:loading="isExecuting" :loading="isExecuting"
:disabled="selectedExecutionRows.length === 0" :disabled="selectedExecutionRows.length === 0"
@click="executeSerialInvoices" @click="executeSerialInvoices"
> >
Jetzt ausführen Jetzt ausführen
</UButton> </UButton>
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
<USlideover v-model="showExecutionsSlideover" :ui="{ width: 'w-screen max-w-md' }"> <USlideover v-model:open="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 #body>
<template #header> <UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<div class="flex items-center justify-between"> <template #header>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <div class="flex items-center justify-between">
Alle Ausführungen <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
</h3> Alle Ausführungen
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionsSlideover = false" /> </h3>
</div> <UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionsSlideover = false" />
</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>
<div class="text-xs text-gray-500 grid grid-cols-2 gap-2 mt-2"> </template>
<div>
<UIcon name="i-heroicons-check-circle" class="text-green-500 w-3 h-3 align-text-bottom" /> <div class="space-y-4 overflow-y-auto h-full p-1">
{{exec.summary}} <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>
</div> </div>
</div> </UCard>
</UCard> </template>
</USlideover> </USlideover>
</template> </template>

View File

@@ -94,7 +94,7 @@ const openBankstatements = () => {
<UButton <UButton
@click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)" @click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)"
variant="outline" variant="outline"
color="rose" color="error"
:disabled="links.find(i => i.type === 'cancellationInvoices')" :disabled="links.find(i => i.type === 'cancellationInvoices')"
> >
Stornieren Stornieren

View File

@@ -13,6 +13,7 @@ const itemInfo = ref({
}) })
const showDocument = ref(false) const showDocument = ref(false)
const uri = ref("") const uri = ref("")
const openTab = ref("0")
const setupPage = async () => { const setupPage = async () => {
letterheads.value = await useEntities("letterheads").select("*") letterheads.value = await useEntities("letterheads").select("*")
@@ -23,7 +24,9 @@ const setupPage = async () => {
setupPage() setupPage()
const onChangeTab = (index) => { const onChangeTab = (index) => {
if(index === 1) { openTab.value = String(index)
if(String(index) === "1") {
generateDocument() generateDocument()
} }
} }
@@ -78,8 +81,8 @@ const contentChanged = (content) => {
<UDashboardNavbar title="Anschreiben bearbeiten"/> <UDashboardNavbar title="Anschreiben bearbeiten"/>
{{itemInfo}} {{itemInfo}}
<UDashboardPanelContent> <UDashboardPanelContent>
<UTabs @change="onChangeTab" :items="[{label: 'Editor'},{label: 'Vorschau'}]"> <UTabs v-model="openTab" @update:model-value="onChangeTab" :items="[{label: 'Editor'},{label: 'Vorschau'}]">
<template #item="{item}"> <template #content="{item}">
<div v-if="item.label === 'Editor'"> <div v-if="item.label === 'Editor'">
<Tiptap <Tiptap
class="mt-3" class="mt-3"

View File

@@ -162,7 +162,7 @@ const sendEmail = async () => {
if(!res.success) { 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 { } else {
navigateTo("/") navigateTo("/")
@@ -210,34 +210,34 @@ const sendEmail = async () => {
<div class="scrollContainer mt-3"> <div class="scrollContainer mt-3">
<div class="flex-col flex w-full"> <div class="flex-col flex w-full">
<UFormGroup <UFormField
label="Absender" label="Absender"
> >
<USelectMenu <USelectMenu
:options="emailAccounts" :items="emailAccounts"
option-attribute="email" label-key="email"
value-attribute="id" value-key="id"
v-model="emailData.account" v-model="emailData.account"
/> />
</UFormGroup> </UFormField>
<UDivider class="my-3"/> <USeparator class="my-3"/>
<UFormGroup <UFormField
label="Empfänger" label="Empfänger"
> >
<UInput <UInput
class="w-full my-1" class="w-full my-1"
v-model="emailData.to" v-model="emailData.to"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Kopie" label="Kopie"
> >
<UInput <UInput
class="w-full my-1" class="w-full my-1"
v-model="emailData.cc" v-model="emailData.cc"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Blindkopie" label="Blindkopie"
> >
<UInput <UInput
@@ -245,17 +245,17 @@ const sendEmail = async () => {
placeholder="" placeholder=""
v-model="emailData.bcc" v-model="emailData.bcc"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Betreff" label="Betreff"
> >
<UInput <UInput
class="w-full my-1" class="w-full my-1"
v-model="emailData.subject" v-model="emailData.subject"
/> />
</UFormGroup> </UFormField>
</div> </div>
<UDivider class="my-3"/> <USeparator class="my-3"/>
<div id="parentAttachments" class="flex flex-col justify-center mt-3"> <div id="parentAttachments" class="flex flex-col justify-center mt-3">
<span class="font-medium mb-2 text-xl">Anhänge</span> <span class="font-medium mb-2 text-xl">Anhänge</span>
<!-- <UIcon <!-- <UIcon

View File

@@ -39,7 +39,7 @@ const createExport = async () => {
:loading="true" :loading="true"
v-model="selected" v-model="selected"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }" :loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:rows="createddocuments" /> :data="createddocuments" />
</template> </template>
<style scoped> <style scoped>

View File

@@ -89,7 +89,7 @@ const createExport = async () => {
if(res.success) { if(res.success) {
toast.add({title: "Export wird erstellt. Sie erhalten eine Benachrichtigung sobald es soweit ist."}) toast.add({title: "Export wird erstellt. Sie erhalten eine Benachrichtigung sobald es soweit ist."})
} else { } 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> </UDashboardNavbar>
<UTable <UTable
:rows="filteredExports" :data="filteredExports"
:columns="[ :columns="normalizeTableColumns([
{ key: 'created_at', label: 'Erstellt am' }, { key: 'created_at', label: 'Erstellt am' },
{ key: 'start_date', label: 'Start' }, { key: 'start_date', label: 'Start' },
{ key: 'end_date', label: 'Ende' }, { key: 'end_date', label: 'Ende' },
{ key: 'valid_until', label: 'Gültig bis' }, { key: 'valid_until', label: 'Gültig bis' },
{ key: 'type', label: 'Typ' }, { key: 'type', label: 'Typ' },
{ key: 'download', label: 'Download' }, { key: 'download', label: 'Download' },
]" ])"
> >
<template #created_at-data="{row}"> <template #created_at-cell="{row}">
{{dayjs(row.created_at).format("DD.MM.YYYY HH:mm")}} {{dayjs(row.original.created_at).format("DD.MM.YYYY HH:mm")}}
</template> </template>
<template #start_date-data="{row}"> <template #start_date-cell="{row}">
{{dayjs(row.start_date).format("DD.MM.YYYY HH:mm")}} {{dayjs(row.original.start_date).format("DD.MM.YYYY HH:mm")}}
</template> </template>
<template #end_date-data="{row}"> <template #end_date-cell="{row}">
{{dayjs(row.end_date).format("DD.MM.YYYY HH:mm")}} {{dayjs(row.original.end_date).format("DD.MM.YYYY HH:mm")}}
</template> </template>
<template #valid_until-data="{row}"> <template #valid_until-cell="{row}">
{{dayjs(row.valid_until).format("DD.MM.YYYY HH:mm")}} {{dayjs(row.original.valid_until).format("DD.MM.YYYY HH:mm")}}
</template> </template>
<template #download-data="{row}"> <template #download-cell="{row}">
<UButton @click="downloadFile(row)">Download</UButton> <UButton @click="downloadFile(row.original)">Download</UButton>
</template> </template>
</UTable> </UTable>
<UModal v-model="showCreateExportModal"> <UModal v-model:open="showCreateExportModal">
<UCard> <template #content>
<template #header> <UCard>
Export erstellen <template #header>
</template> 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="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> <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> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<UFormGroup label="Start:" class="flex-1"> <UFormField label="Start:" class="flex-1">
<UPopover :popper="{ placement: 'bottom-start' }"> <UPopover :popper="{ placement: 'bottom-start' }">
<UButton <UButton
icon="i-heroicons-calendar-days-20-solid" icon="i-heroicons-calendar-days-20-solid"
@@ -192,9 +193,9 @@ const createExport = async () => {
<LazyDatePicker v-model="createExportData.start_date" @close="close" /> <LazyDatePicker v-model="createExportData.start_date" @close="close" />
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
<UFormGroup label="Ende:" class="flex-1"> <UFormField label="Ende:" class="flex-1">
<UPopover :popper="{ placement: 'bottom-start' }"> <UPopover :popper="{ placement: 'bottom-start' }">
<UButton <UButton
icon="i-heroicons-calendar-days-20-solid" icon="i-heroicons-calendar-days-20-solid"
@@ -206,21 +207,22 @@ const createExport = async () => {
<LazyDatePicker v-model="createExportData.end_date" @close="close" /> <LazyDatePicker v-model="createExportData.end_date" @close="close" />
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
<UButton @click="createExport"> <UButton @click="createExport">
Erstellen Erstellen
</UButton> </UButton>
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@@ -350,8 +350,8 @@ const syncdokubox = async () => {
<UBreadcrumb :links="breadcrumbLinks"/> <UBreadcrumb :links="breadcrumbLinks"/>
</template> </template>
<template #right> <template #right>
<USelectMenu v-model="displayMode" :options="displayModes" value-attribute="key" class="w-32" :ui-menu="{ zIndex: 'z-50' }"> <USelectMenu v-model="displayMode" :items="displayModes" value-key="key" class="w-32" :content="{ zIndex: 'z-50' }">
<template #label> <template #default>
<UIcon :name="displayModes.find(i => i.key === displayMode).icon" class="w-4 h-4"/> <UIcon :name="displayModes.find(i => i.key === displayMode).icon" class="w-4 h-4"/>
<span>{{ displayModes.find(i => i.key === displayMode).label }}</span> <span>{{ displayModes.find(i => i.key === displayMode).label }}</span>
</template> </template>
@@ -371,9 +371,9 @@ const syncdokubox = async () => {
<UProgress animation="carousel" class="w-1/2"/> <UProgress animation="carousel" class="w-1/2"/>
</div> </div>
<div v-else> <div v-else>
<div v-if="displayMode === 'list'"> <div v-if="displayMode === 'list'">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0 z-20"> <thead class="bg-gray-50 dark:bg-gray-800 sticky top-0 z-20">
<tr> <tr>
<th class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold sm:pl-6">Name</th> <th class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold sm:pl-6">Name</th>
@@ -412,6 +412,11 @@ const syncdokubox = async () => {
</UDropdown> </UDropdown>
</td> </td>
</tr> </tr>
<tr v-if="renderedFileList.length === 0">
<td colspan="3" class="px-6 py-16 text-center text-sm text-gray-500">
Keine Dateien oder Ordner vorhanden.
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -438,34 +443,41 @@ const syncdokubox = async () => {
/> />
<span class="text-xs font-medium text-center truncate w-full">{{ entry.label }}</span> <span class="text-xs font-medium text-center truncate w-full">{{ entry.label }}</span>
</div> </div>
<div
v-if="renderedFileList.length === 0"
class="col-span-full rounded-xl border border-dashed border-gray-300 dark:border-gray-700 px-6 py-16 text-center text-sm text-gray-500"
>
Keine Dateien oder Ordner vorhanden.
</div>
</div> </div>
</div> </div>
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model="createFolderModalOpen"> <UModal v-model:open="createFolderModalOpen">
<UCard> <template #content>
<template #header><h3 class="font-bold">Ordner erstellen</h3></template> <UCard>
<template #header><h3 class="font-bold">Ordner erstellen</h3></template>
<div class="space-y-4"> <div class="space-y-4">
<UFormGroup label="Name" required> <UFormField label="Name" required>
<UInput v-model="createFolderData.name" autofocus @keyup.enter="createFolder"/> <UInput v-model="createFolderData.name" autofocus @keyup.enter="createFolder"/>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Standard Dateityp (Tag)" label="Standard Dateityp (Tag)"
:help="isParentTypeMandatory ? 'Vom übergeordneten Ordner vorgegeben' : ''" :help="isParentTypeMandatory ? 'Vom übergeordneten Ordner vorgegeben' : ''"
> >
<USelectMenu <USelectMenu
v-model="createFolderData.standardFiletype" v-model="createFolderData.standardFiletype"
:options="filetags" :items="filetags"
value-attribute="id" value-key="id"
option-attribute="name" label-key="name"
placeholder="Kein Standardtyp" placeholder="Kein Standardtyp"
searchable searchable
clear-search-on-close clear-search-on-close
:disabled="isParentTypeMandatory" :disabled="isParentTypeMandatory"
/> />
</UFormGroup> </UFormField>
<UCheckbox <UCheckbox
v-model="createFolderData.standardFiletypeIsOptional" v-model="createFolderData.standardFiletypeIsOptional"
@@ -474,27 +486,30 @@ const syncdokubox = async () => {
/> />
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<UButton color="gray" @click="createFolderModalOpen = false">Abbrechen</UButton> <UButton color="gray" @click="createFolderModalOpen = false">Abbrechen</UButton>
<UButton color="primary" @click="createFolder">Erstellen</UButton> <UButton color="primary" @click="createFolder">Erstellen</UButton>
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
<UModal v-model="renameModalOpen"> <UModal v-model:open="renameModalOpen">
<UCard> <template #content>
<template #header><h3 class="font-bold">Umbenennen</h3></template> <UCard>
<UFormGroup label="Neuer Name"> <template #header><h3 class="font-bold">Umbenennen</h3></template>
<UInput v-model="renameData.name" autofocus @keyup.enter="updateName"/> <UFormField label="Neuer Name">
</UFormGroup> <UInput v-model="renameData.name" autofocus @keyup.enter="updateName"/>
<template #footer> </UFormField>
<div class="flex justify-end gap-2"> <template #footer>
<UButton color="gray" @click="renameModalOpen = false">Abbrechen</UButton> <div class="flex justify-end gap-2">
<UButton color="primary" @click="updateName">Speichern</UButton> <UButton color="gray" @click="renameModalOpen = false">Abbrechen</UButton>
</div> <UButton color="primary" @click="updateName">Speichern</UButton>
</template> </div>
</UCard> </template>
</UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -157,7 +157,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
toast.add({ toast.add({
title: "Buchen nicht möglich", title: "Buchen nicht möglich",
description: "Bitte beheben Sie zuerst die rot markierten Pflichtfehler.", description: "Bitte beheben Sie zuerst die rot markierten Pflichtfehler.",
color: "rose" color: "error"
}) })
return return
} }
@@ -220,7 +220,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<template #right> <template #right>
<ArchiveButton <ArchiveButton
v-if="mode !== 'show'" v-if="mode !== 'show'"
color="rose" color="error"
variant="outline" variant="outline"
type="incominginvoices" type="incominginvoices"
@confirmed="useEntities('incominginvoices').archive(route.params.id)" @confirmed="useEntities('incominginvoices').archive(route.params.id)"
@@ -286,7 +286,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<UAlert <UAlert
v-if="findIncomingInvoiceErrors.length > 0" v-if="findIncomingInvoiceErrors.length > 0"
title="Prüfung erforderlich" title="Prüfung erforderlich"
:color="hasBlockingIncomingInvoiceErrors ? 'rose' : 'orange'" :color="hasBlockingIncomingInvoiceErrors ? 'error' : 'orange'"
variant="soft" variant="soft"
icon="i-heroicons-exclamation-triangle" icon="i-heroicons-exclamation-triangle"
> >
@@ -323,14 +323,14 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h3 class="font-semibold">Stammdaten</h3> <h3 class="font-semibold">Stammdaten</h3>
<div class="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg"> <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> <UButton size="xs" :variant="!itemInfo.expense ? 'solid' : 'ghost'" color="emerald" @click="itemInfo.expense = false" :disabled="mode === 'show'">Einnahme</UButton>
</div> </div>
</div> </div>
</template> </template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <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"> <div class="flex gap-2">
<USelectMenu <USelectMenu
class="w-full" class="w-full"
@@ -354,7 +354,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
v-if="mode !== 'show'" v-if="mode !== 'show'"
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
variant="outline" variant="outline"
color="rose" color="error"
:disabled="!itemInfo.vendor" :disabled="!itemInfo.vendor"
@click="itemInfo.vendor = null" @click="itemInfo.vendor = null"
/> />
@@ -365,37 +365,37 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
@return-data="(data) => itemInfo.vendor = data.id" @return-data="(data) => itemInfo.vendor = data.id"
/> />
</div> </div>
</UFormGroup> </UFormField>
<UFormGroup label="Rechnungsnummer"> <UFormField label="Rechnungsnummer">
<UInput v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" /> <UInput v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" />
</UFormGroup> </UFormField>
<UFormGroup label="Zahlart"> <UFormField label="Zahlart">
<USelectMenu v-model="itemInfo.paymentType" :options="['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']" :disabled="mode === 'show'" /> <USelectMenu v-model="itemInfo.paymentType" :options="['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']" :disabled="mode === 'show'" />
</UFormGroup> </UFormField>
<UFormGroup label="Rechnungsdatum"> <UFormField label="Rechnungsdatum">
<UPopover :popper="{ placement: 'bottom-start' }"> <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'" /> <UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
<template #panel="{ close }"> <template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.date" @close="() => { if(!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date; close() }" /> <LazyDatePicker v-model="itemInfo.date" @close="() => { if(!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date; close() }" />
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
<UFormGroup label="Fälligkeitsdatum"> <UFormField label="Fälligkeitsdatum">
<UPopover :popper="{ placement: 'bottom-start' }"> <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'" /> <UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
<template #panel="{ close }"> <template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" /> <LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
</template> </template>
</UPopover> </UPopover>
</UFormGroup> </UFormField>
<UFormGroup label="Beschreibung / Notiz" class="md:col-span-2"> <UFormField label="Beschreibung / Notiz" class="md:col-span-2">
<UTextarea v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" /> <UTextarea v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
</UFormGroup> </UFormField>
</div> </div>
</UCard> </UCard>
@@ -404,7 +404,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<h3 class="font-semibold text-lg">Positionen</h3> <h3 class="font-semibold text-lg">Positionen</h3>
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
<span :class="{'font-bold': !useNetMode, 'opacity-50': useNetMode}">Brutto</span> <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> <span :class="{'font-bold': useNetMode, 'opacity-50': !useNetMode}">Netto Eingabe</span>
</div> </div>
</div> </div>
@@ -419,7 +419,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<UButton <UButton
v-if="itemInfo.accounts.length > 1 && mode !== 'show'" v-if="itemInfo.accounts.length > 1 && mode !== 'show'"
icon="i-heroicons-trash" icon="i-heroicons-trash"
color="rose" color="error"
variant="ghost" variant="ghost"
size="xs" size="xs"
class="absolute top-2 right-2" class="absolute top-2 right-2"
@@ -428,7 +428,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<div class="grid grid-cols-12 gap-3"> <div class="grid grid-cols-12 gap-3">
<div class="col-span-12 md:col-span-6"> <div class="col-span-12 md:col-span-6">
<UFormGroup label="Konto / Kategorie"> <UFormField label="Konto / Kategorie">
<USelectMenu <USelectMenu
v-model="item.account" v-model="item.account"
:options="accounts" :options="accounts"
@@ -446,11 +446,11 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
{{ accounts.find(a => a.id === item.account)?.label || 'Auswählen' }} {{ accounts.find(a => a.id === item.account)?.label || 'Auswählen' }}
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-12 md:col-span-6"> <div class="col-span-12 md:col-span-6">
<UFormGroup label="Kostenstelle"> <UFormField label="Kostenstelle">
<USelectMenu <USelectMenu
v-model="item.costCentre" v-model="item.costCentre"
:options="costcentres" :options="costcentres"
@@ -464,11 +464,11 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
{{ costcentres.find(c => c.id === item.costCentre)?.name || 'Keine' }} {{ costcentres.find(c => c.id === item.costCentre)?.name || 'Keine' }}
</template> </template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-12 md:col-span-3"> <div class="col-span-12 md:col-span-3">
<UFormGroup label="Betrag (Netto)"> <UFormField label="Betrag (Netto)">
<UInput <UInput
type="number" type="number"
step="0.01" step="0.01"
@@ -478,11 +478,11 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
> >
<template #trailing>€</template> <template #trailing>€</template>
</UInput> </UInput>
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-12 md:col-span-3"> <div class="col-span-12 md:col-span-3">
<UFormGroup label="Betrag (Brutto)"> <UFormField label="Betrag (Brutto)">
<UInput <UInput
type="number" type="number"
step="0.01" step="0.01"
@@ -492,11 +492,11 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
> >
<template #trailing>€</template> <template #trailing>€</template>
</UInput> </UInput>
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-6 md:col-span-3"> <div class="col-span-6 md:col-span-3">
<UFormGroup label="Steuerschlüssel"> <UFormField label="Steuerschlüssel">
<USelectMenu <USelectMenu
v-model="item.taxType" v-model="item.taxType"
:options="taxOptions" :options="taxOptions"
@@ -505,15 +505,15 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
:disabled="mode === 'show'" :disabled="mode === 'show'"
@change="recalculateItem(item, 'taxType')" @change="recalculateItem(item, 'taxType')"
/> />
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-6 md:col-span-3"> <div class="col-span-6 md:col-span-3">
<UFormGroup label="Steuerbetrag" help="Automatisch berechnet"> <UFormField label="Steuerbetrag" help="Automatisch berechnet">
<UInput :model-value="item.amountTax" disabled color="gray" > <UInput :model-value="item.amountTax" disabled color="gray" >
<template #trailing>€</template> <template #trailing>€</template>
</UInput> </UInput>
</UFormGroup> </UFormField>
</div> </div>
<div class="col-span-12 flex justify-end gap-2"> <div class="col-span-12 flex justify-end gap-2">

View File

@@ -191,7 +191,7 @@ const selectIncomingInvoice = (invoice) => {
<UButton <UButton
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
variant="outline" variant="outline"
color="rose" color="error"
@click="clearSearchString()" @click="clearSearchString()"
v-if="searchString.length > 0" v-if="searchString.length > 0"
/> />
@@ -203,15 +203,15 @@ const selectIncomingInvoice = (invoice) => {
<USelectMenu <USelectMenu
v-model="selectedColumns" v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
:options="dataType.templateColumns.filter(i => !i.disabledInTable)" :items="dataType.templateColumns.filter(i => !i.disabledInTable)"
multiple multiple
class="hidden lg:block" class="hidden lg:block"
by="key" by="key"
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'" :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)" @change="tempStore.modifyColumns(type,selectedColumns)"
> >
<template #label> <template #default>
Spalten Spalten
</template> </template>
</USelectMenu> </USelectMenu>
@@ -220,11 +220,11 @@ const selectIncomingInvoice = (invoice) => {
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
multiple multiple
v-model="selectedFilters" v-model="selectedFilters"
:options="selectableFilters" :items="selectableFilters"
:color="selectedFilters.length > 0 ? 'primary' : 'white'" :color="selectedFilters.length > 0 ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }" :content="{ width: 'min-w-max' }"
> >
<template #label> <template #default>
Filter Filter
</template> </template>
</USelectMenu> </USelectMenu>
@@ -244,42 +244,42 @@ const selectIncomingInvoice = (invoice) => {
{{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}} {{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}}
</UBadge> </UBadge>
</template> </template>
<template #item="{item}"> <template #content="{item}">
<div style="height: 80dvh; overflow-y: scroll"> <div style="height: 80dvh; overflow-y: scroll">
<UTable <UTable
v-model:sort="sort" v-model:sorting="sort"
sort-mode="manual" sort-mode="manual"
@update:sort="setupPage" @update:sorting="setupPage"
:rows="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )" :data="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => selectIncomingInvoice(i) " :on-select="(i) => selectIncomingInvoice(i) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
> >
<template #reference-data="{row}"> <template #reference-cell="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.reference}}</span> <span v-if="row.original === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.original.reference}}</span>
<span v-else>{{row.reference}}</span> <span v-else>{{row.original.reference}}</span>
</template> </template>
<template #state-data="{row}"> <template #state-cell="{row}">
<span v-if="row.state === 'Vorbereitet'" class="text-cyan-500">{{row.state}}</span> <span v-if="row.original.state === 'Vorbereitet'" class="text-cyan-500">{{row.original.state}}</span>
<span v-else-if="row.state === 'Entwurf'" class="text-red-500">{{row.state}}</span> <span v-else-if="row.original.state === 'Entwurf'" class="text-red-500">{{row.original.state}}</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>
<template #date-data="{row}"> <template #date-cell="{row}">
{{dayjs(row.date).format("DD.MM.YYYY")}} {{dayjs(row.original.date).format("DD.MM.YYYY")}}
</template> </template>
<template #vendor-data="{row}"> <template #vendor-cell="{row}">
{{row.vendor ? row.vendor.name : ""}} {{row.original.vendor ? row.original.vendor.name : ""}}
</template> </template>
<template #amount-data="{row}"> <template #amount-cell="{row}">
{{displayCurrency(sum.getIncomingInvoiceSum(row))}} {{displayCurrency(sum.getIncomingInvoiceSum(row.original))}}
</template> </template>
<template #dueDate-data="{row}"> <template #dueDate-cell="{row}">
<span v-if="row.dueDate">{{dayjs(row.dueDate).format("DD.MM.YYYY")}}</span> <span v-if="row.original.dueDate">{{dayjs(row.original.dueDate).format("DD.MM.YYYY")}}</span>
</template> </template>
<template #paid-data="{row}"> <template #paid-cell="{row}">
<span v-if="isPaid(row)" class="text-primary-500">Bezahlt</span> <span v-if="isPaid(row.original)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span> <span v-else class="text-rose-600">Offen</span>
</template> </template>
</UTable> </UTable>

View File

@@ -7,6 +7,7 @@ import DisplayOpenBalances from "~/components/displayOpenBalances.vue"
import DisplayBankaccounts from "~/components/displayBankaccounts.vue" import DisplayBankaccounts from "~/components/displayBankaccounts.vue"
import DisplayProjectsInPhases from "~/components/displayProjectsInPhases.vue" import DisplayProjectsInPhases from "~/components/displayProjectsInPhases.vue"
import DisplayOpenTasks from "~/components/displayOpenTasks.vue" import DisplayOpenTasks from "~/components/displayOpenTasks.vue"
import DisplayTaxSummary from "~/components/displayTaxSummary.vue"
setPageLayout("default") setPageLayout("default")
@@ -68,6 +69,15 @@ const DASHBOARD_WIDGETS = [
defaultLayout: { x: 0, y: 7, w: 4, h: 3 }, defaultLayout: { x: 0, y: 7, w: 4, h: 3 },
minW: 3, minW: 3,
minH: 3 minH: 3
},
{
id: "tax-summary",
title: "USt aktuell",
description: "USt, Vorsteuer und Saldo des aktuellen Zeitraums",
component: markRaw(DisplayTaxSummary),
defaultLayout: { x: 4, y: 7, w: 4, h: 3 },
minW: 3,
minH: 3
} }
] ]
@@ -372,7 +382,7 @@ onBeforeUnmount(() => {
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<div v-if="visibleWidgets.length > 0" ref="gridElement" class="grid-stack dashboard-grid overflow-y-auto h-80"> <div v-if="visibleWidgets.length > 0" ref="gridElement" class="grid-stack dashboard-grid overflow-y-auto">
<div <div
v-for="widget in visibleWidgets" v-for="widget in visibleWidgets"
:key="widget.id" :key="widget.id"
@@ -385,9 +395,9 @@ onBeforeUnmount(() => {
:gs-min-w="widget.minW" :gs-min-w="widget.minW"
:gs-min-h="widget.minH" :gs-min-h="widget.minH"
> >
<div class="grid-stack-item-content dashboard-grid-item"> <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-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="dashboard-widget-header border-b border-gray-200 dark:border-gray-800">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0"> <div class="min-w-0">
<div :class="['dashboard-widget-drag-handle font-semibold', isEditMode ? 'cursor-move' : 'cursor-default']"> <div :class="['dashboard-widget-drag-handle font-semibold', isEditMode ? 'cursor-move' : 'cursor-default']">
@@ -430,7 +440,7 @@ onBeforeUnmount(() => {
</div> </div>
</div> </div>
<div class="rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-10 text-center"> <div v-else class="rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-10 text-center">
<p class="text-sm"> <p class="text-sm">
Es sind aktuell keine Dashboard-Karten sichtbar. Es sind aktuell keine Dashboard-Karten sichtbar.
</p> </p>
@@ -439,55 +449,57 @@ onBeforeUnmount(() => {
</UButton> </UButton>
</div> </div>
<UModal v-model="manageCardsOpen"> <UModal v-model:open="manageCardsOpen">
<UCard> <template #content>
<template #header> <UCard>
<div class="flex items-center justify-between gap-3"> <template #header>
<div> <div class="flex items-center justify-between gap-3">
<h2 class="font-semibold">Dashboard-Karten</h2> <div>
<p class="text-sm"> <h2 class="font-semibold">Dashboard-Karten</h2>
Karten ein- oder ausblenden und bei Bedarf auf das Standardlayout zurücksetzen. <p class="text-sm">
</p> Karten ein- oder ausblenden und bei Bedarf auf das Standardlayout zurücksetzen.
</p>
</div>
<UButton color="gray" variant="ghost" icon="i-heroicons-arrow-path" @click="resetDashboard">
Zurücksetzen
</UButton>
</div> </div>
<UButton color="gray" variant="ghost" icon="i-heroicons-arrow-path" @click="resetDashboard"> </template>
Zurücksetzen
</UButton>
</div>
</template>
<div class="space-y-3"> <div class="space-y-3">
<div <div
v-for="definition in DASHBOARD_WIDGETS" v-for="definition in DASHBOARD_WIDGETS"
:key="definition.id" :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" class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 dark:border-gray-800 px-4 py-3"
> >
<div> <div>
<p class="font-medium">{{ definition.title }}</p> <p class="font-medium">{{ definition.title }}</p>
<p class="text-sm">{{ definition.description }}</p> <p class="text-sm">{{ definition.description }}</p>
</div>
<UButton
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)"
>
Hinzufügen
</UButton>
</div> </div>
<UButton
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)"
>
Hinzufügen
</UButton>
</div> </div>
</div> </UCard>
</UCard> </template>
</UModal> </UModal>
</div> </div>
</template> </template>

View File

@@ -16,7 +16,7 @@ const doLogin = async (data:any) => {
await router.push("/") await router.push("/")
} catch (err: any) { } catch (err: any) {
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"}) toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"error"})
} }
} }
</script> </script>

View File

@@ -423,7 +423,7 @@ onMounted(() => {
Urlaub Urlaub
</UButton> </UButton>
<UButton <UButton
color="rose" color="error"
variant="soft" variant="soft"
icon="i-heroicons-heart" icon="i-heroicons-heart"
@click="openAbsenceModal('sick')" @click="openAbsenceModal('sick')"
@@ -453,25 +453,26 @@ onMounted(() => {
/> />
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model="isAbsenceModalOpen"> <UModal v-model:open="isAbsenceModalOpen">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <template #content>
<template #header> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<div class="flex items-center justify-between"> <template #header>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <div class="flex items-center justify-between">
{{ absenceModalTitle }} <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
</h3> {{ absenceModalTitle }}
<UButton </h3>
color="gray" <UButton
variant="ghost" color="gray"
icon="i-heroicons-x-mark-20-solid" variant="ghost"
class="-my-1" icon="i-heroicons-x-mark-20-solid"
@click="isAbsenceModalOpen = false" class="-my-1"
/> @click="isAbsenceModalOpen = false"
</div> />
</template> </div>
</template>
<div class="space-y-4"> <div class="space-y-4">
<UFormGroup label="Profil"> <UFormField label="Profil">
<USelectMenu <USelectMenu
v-model="absenceForm.userId" v-model="absenceForm.userId"
:options="profileOptions" :options="profileOptions"
@@ -479,51 +480,52 @@ onMounted(() => {
option-attribute="label" option-attribute="label"
searchable searchable
/> />
</UFormGroup> </UFormField>
<UFormGroup label="Typ"> <UFormField label="Typ">
<USelectMenu <USelectMenu
v-model="absenceForm.type" v-model="absenceForm.type"
:options="absenceTypeOptions" :options="absenceTypeOptions"
value-attribute="value" value-attribute="value"
option-attribute="label" option-attribute="label"
/> />
</UFormGroup> </UFormField>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<UFormGroup label="Start"> <UFormField label="Start">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UInput v-model="absenceForm.startDate" type="date" class="flex-1" /> <UInput v-model="absenceForm.startDate" type="date" class="flex-1" />
<UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('startDate')" /> <UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('startDate')" />
</div> </div>
</UFormGroup> </UFormField>
<UFormGroup label="Ende"> <UFormField label="Ende">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UInput v-model="absenceForm.endDate" type="date" class="flex-1" /> <UInput v-model="absenceForm.endDate" type="date" class="flex-1" />
<UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('endDate')" /> <UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('endDate')" />
</div> </div>
</UFormGroup> </UFormField>
</div> </div>
<UFormGroup label="Notiz"> <UFormField label="Notiz">
<UTextarea <UTextarea
v-model="absenceForm.description" v-model="absenceForm.description"
:placeholder="absenceForm.type === 'sick' ? 'z. B. Krankmeldung eingegangen' : 'z. B. Sommerurlaub'" :placeholder="absenceForm.type === 'sick' ? 'z. B. Krankmeldung eingegangen' : 'z. B. Sommerurlaub'"
/> />
</UFormGroup> </UFormField>
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<UButton color="gray" variant="soft" @click="isAbsenceModalOpen = false"> <UButton color="gray" variant="soft" @click="isAbsenceModalOpen = false">
Abbrechen Abbrechen
</UButton> </UButton>
<UButton color="primary" :loading="savingAbsence" @click="saveAbsence"> <UButton color="primary" :loading="savingAbsence" @click="saveAbsence">
Speichern Speichern
</UButton> </UButton>
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</div> </div>
</template> </template>

View File

@@ -24,7 +24,7 @@ const doChange = async (data:any) => {
await auth.logout() await auth.logout()
return navigateTo("/login") return navigateTo("/login")
} catch (err: any) { } 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"})
} }
} }
</script> </script>

View File

@@ -22,7 +22,7 @@ const doReset = async (data:any) => {
toast.add({title:"Zurücksetzen erfolgreich"}) toast.add({title:"Zurücksetzen erfolgreich"})
return navigateTo("/login") return navigateTo("/login")
} catch (err: any) { } catch (err: any) {
toast.add({title:"Problem beim zurücksetzen",color:"rose"}) toast.add({title:"Problem beim zurücksetzen",color:"error"})
} }
} }
</script> </script>

View File

@@ -9,18 +9,20 @@ defineShortcuts({
router.push("/projecttypes") router.push("/projecttypes")
}, },
'arrowleft': () => { 'arrowleft': () => {
if(openTab.value > 0){ const currentIndex = Number(openTab.value)
openTab.value -= 1 if(currentIndex > 0){
openTab.value = String(currentIndex - 1)
} }
}, },
'arrowright': () => { 'arrowright': () => {
if(openTab.value < 3) { const currentIndex = Number(openTab.value)
openTab.value += 1 if(currentIndex < 3) {
openTab.value = String(currentIndex + 1)
} }
}, },
}) })
const openTab = ref(0) const openTab = ref("0")
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const toast = useToast() const toast = useToast()
@@ -129,7 +131,7 @@ const addPhase = () => {
v-if="itemInfo.id && mode == 'show'" v-if="itemInfo.id && mode == 'show'"
v-model="openTab" v-model="openTab"
> >
<template #item="{ item }"> <template #content="{ item }">
<div v-if="item.label === 'Informationen'" class="flex flex-row"> <div v-if="item.label === 'Informationen'" class="flex flex-row">
<div class="w-1/2 mr-3"> <div class="w-1/2 mr-3">
<UCard class="mt-5"> <UCard class="mt-5">
@@ -152,24 +154,24 @@ const addPhase = () => {
<UForm v-else-if="mode === 'edit' || mode === 'create'"> <UForm v-else-if="mode === 'edit' || mode === 'create'">
<UAlert <UAlert
color="rose" color="error"
variant="outline" variant="outline"
class="mb-5" class="mb-5"
v-if="mode === 'edit'" v-if="mode === 'edit'"
description="Achtung Änderungen an diesem Projekttypen betreffen nur Projekte die damit neu erstellt werden. Bestehende Projekte bleiben unverändert." description="Achtung Änderungen an diesem Projekttypen betreffen nur Projekte die damit neu erstellt werden. Bestehende Projekte bleiben unverändert."
/> />
<UFormGroup <UFormField
label="Name:" label="Name:"
> >
<UInput <UInput
v-model="itemInfo.name" v-model="itemInfo.name"
/> />
</UFormGroup> </UFormField>
<UDivider class="mt-5"> <USeparator class="mt-5">
Initiale Phasen Initiale Phasen
</UDivider> </USeparator>
<UButton <UButton
class="mt-3" class="mt-3"
@click="addPhase" @click="addPhase"
@@ -247,44 +249,46 @@ const addPhase = () => {
{{ button.label }} {{ button.label }}
</UButton> </UButton>
<UModal v-model="openQuickActionModal"> <UModal v-model:open="openQuickActionModal">
<UCard> <template #content>
<div class="flex items-center justify-between"> <UCard>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <div class="flex items-center justify-between">
Schnellaktion hinzufügen <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
</h3> Schnellaktion hinzufügen
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="openQuickActionModal = false" /> </h3>
</div> <UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="openQuickActionModal = false" />
<div class="flex flex-col"> </div>
<UButton <div class="flex flex-col">
class="my-1" <UButton
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Angebot',link:'/createDocument/edit/?type=quotes'})">Angebot Erstellen</UButton> class="my-1"
<UButton @click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Angebot',link:'/createDocument/edit/?type=quotes'})">Angebot Erstellen</UButton>
class="my-1" <UButton
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Auftrag',link:'/createDocument/edit/?type=confirmationOrders'})">Auftrag Erstellen</UButton> class="my-1"
<UButton @click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Auftrag',link:'/createDocument/edit/?type=confirmationOrders'})">Auftrag Erstellen</UButton>
class="my-1" <UButton
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Lieferschein',link:'/createDocument/edit/?type=deliveryNotes'})">Lieferschein Erstellen</UButton> class="my-1"
<UButton @click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Lieferschein',link:'/createDocument/edit/?type=deliveryNotes'})">Lieferschein Erstellen</UButton>
class="my-1" <UButton
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Rechnung',link:'/createDocument/edit/?type=invoices'})">Rechnung Erstellen</UButton> class="my-1"
<UButton @click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Rechnung',link:'/createDocument/edit/?type=invoices'})">Rechnung Erstellen</UButton>
class="my-1" <UButton
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Aufgabe',link:'/tasks/create'})">Aufgabe Erstellen</UButton> class="my-1"
<UButton @click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Aufgabe',link:'/tasks/create'})">Aufgabe Erstellen</UButton>
class="my-1" <UButton
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Termin',link:'/events/edit'})">Termin Erstellen</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> </UModal>
</td> </td>
<td> <td>
<UButton <UButton
class="my-2 ml-2" class="my-2 ml-2"
variant="outline" variant="outline"
color="rose" color="error"
@click="itemInfo.initialPhases = itemInfo.initialPhases.filter(i => i !== phase)" @click="itemInfo.initialPhases = itemInfo.initialPhases.filter(i => i !== phase)"
>X</UButton> >X</UButton>
</td> </td>
@@ -301,4 +305,4 @@ const addPhase = () => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -87,16 +87,16 @@ const filteredRows = computed(() => {
</UDashboardNavbar> </UDashboardNavbar>
<UTable <UTable
:rows="filteredRows" :data="filteredRows"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/projecttypes/show/${i.id}`) " :on-select="(i) => router.push(`/projecttypes/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Projekttypen anzuzeigen' }" :empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Projekttypen anzuzeigen' }"
> >
<template #name-data="{row}"> <template #name-cell="{row}">
<span class="text-primary-500 font-bold" v-if="row === filteredRows[selectedItem]">{{ row.name }}</span> <span class="text-primary-500 font-bold" v-if="row.original === filteredRows[selectedItem]">{{ row.original.name }}</span>
<span v-else>{{ row.name }}</span> <span v-else>{{ row.original.name }}</span>
</template> </template>
</UTable> </UTable>
@@ -104,4 +104,4 @@ const filteredRows = computed(() => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -8,13 +8,15 @@ defineShortcuts({
router.push("/roles") router.push("/roles")
}, },
'arrowleft': () => { 'arrowleft': () => {
if(openTab.value > 0){ const currentIndex = Number(openTab.value)
openTab.value -= 1 if(currentIndex > 0){
openTab.value = String(currentIndex - 1)
} }
}, },
'arrowright': () => { 'arrowright': () => {
if(openTab.value < 3) { const currentIndex = Number(openTab.value)
openTab.value += 1 if(currentIndex < 3) {
openTab.value = String(currentIndex + 1)
} }
}, },
}) })
@@ -32,7 +34,7 @@ const mode = ref(route.params.mode || "show")
const itemInfo = ref({ const itemInfo = ref({
rights: [] rights: []
}) })
const openTab = ref(0) const openTab = ref("0")
//Functions //Functions
const setupPage = async () => { const setupPage = async () => {
@@ -120,7 +122,7 @@ setupPage()
class="p-5" class="p-5"
v-model="openTab" v-model="openTab"
> >
<template #item="{item}"> <template #content="{item}">
<div v-if="item.label === 'Informationen'" class="mt-5 flex flex-row"> <div v-if="item.label === 'Informationen'" class="mt-5 flex flex-row">
<div class="w-1/2 mr-5"> <div class="w-1/2 mr-5">
<UCard> <UCard>
@@ -157,15 +159,15 @@ setupPage()
v-else-if="mode == 'edit' || mode == 'create'" v-else-if="mode == 'edit' || mode == 'create'"
class="p-5" class="p-5"
> >
<UFormGroup <UFormField
label="Name:" label="Name:"
> >
<UInput <UInput
v-model="itemInfo.name" v-model="itemInfo.name"
autofocus autofocus
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Rechte:" label="Rechte:"
> >
<USelectMenu <USelectMenu
@@ -176,14 +178,14 @@ setupPage()
multiple multiple
> >
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Beschreibung:" label="Beschreibung:"
> >
<UTextarea <UTextarea
v-model="itemInfo.description" v-model="itemInfo.description"
/> />
</UFormGroup> </UFormField>
</UForm> </UForm>
</template> </template>

View File

@@ -0,0 +1,894 @@
<script setup lang="ts">
const auth = useAuthStore()
const toast = useToast()
const router = useRouter()
type AdminRole = {
id: string
name: string
description?: string | null
tenant_id: number | null
}
type AdminTenant = {
id: number
name: string
short: string
user_count: number
locked?: string | null
}
type AdminUserProfile = {
id: string
user_id: string
tenant_id: number
full_name: string | null
first_name: string
last_name: string
email?: string | null
active: boolean
}
type AdminUser = {
id: string
email: string
display_name: string
multiTenant: boolean
must_change_password: boolean
is_admin: boolean
profile_defaults: {
first_name: string
last_name: string
}
tenant_ids: number[]
role_assignments: { tenant_id: number; role_id: string }[]
profile_assignments?: { tenant_id: number; profile_id?: string | null }[]
profiles: AdminUserProfile[]
}
const loading = ref(true)
const savingUser = ref(false)
const savingTenant = ref(false)
const creatingUser = ref(false)
const creatingTenant = ref(false)
const activeTab = ref("0")
const createUserModalOpen = ref(false)
const createTenantModalOpen = ref(false)
const createdUserPassword = ref("")
const users = ref<AdminUser[]>([])
const tenants = ref<AdminTenant[]>([])
const roles = ref<AdminRole[]>([])
const unassignedProfiles = ref<AdminUserProfile[]>([])
const selectedUserId = ref<string | null>(null)
const selectedTenantId = ref<number | null>(null)
const userForm = ref<AdminUser | null>(null)
const tenantForm = ref<AdminTenant | null>(null)
const createUserForm = ref({
email: "",
password: "",
first_name: "",
last_name: "",
is_admin: false,
multiTenant: true,
})
const createTenantForm = ref({
name: "",
short: "",
})
const tabItems = [
{ label: "Benutzer" },
{ label: "Tenants" },
]
const sortedUsers = computed(() =>
[...users.value].sort((a, b) => a.display_name.localeCompare(b.display_name, "de"))
)
const sortedTenants = computed(() =>
[...tenants.value].sort((a, b) => a.name.localeCompare(b.name, "de"))
)
const tenantOptions = computed(() =>
sortedTenants.value.map((tenant) => ({
label: `${tenant.name} (${tenant.short})`,
value: tenant.id,
}))
)
const userTableColumns = [
{ key: "display_name", label: "Benutzer" },
{ key: "email", label: "E-Mail" },
{ 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) => ({
...user,
tenant_count: user.tenant_ids.length,
is_admin: user.is_admin ? "Ja" : "Nein",
}))
)
const tenantTableRows = computed(() => sortedTenants.value)
const getRoleOptionsForTenant = (tenantId: number) =>
roles.value
.filter((role) => role.tenant_id === null || role.tenant_id === tenantId)
.map((role) => ({
label: role.tenant_id === null ? `${role.name} (global)` : role.name,
value: role.id,
}))
const getUsersForTenant = (tenantId: number) =>
sortedUsers.value.filter((user) => user.tenant_ids.includes(tenantId))
const getFreeProfilesForTenant = (tenantId: number) =>
unassignedProfiles.value.filter((profile) => profile.tenant_id === tenantId)
const cloneUser = (user: AdminUser): AdminUser => ({
...user,
profile_defaults: { ...user.profile_defaults },
tenant_ids: [...(user.tenant_ids || [])],
role_assignments: [...(user.role_assignments || [])],
profile_assignments: [...(user.profile_assignments || [])],
profiles: [...(user.profiles || [])],
})
const cloneTenant = (tenant: AdminTenant): AdminTenant => ({
...tenant,
})
const normalizeUserAssignments = () => {
if (!userForm.value) return
const uniqueTenantIds = Array.from(new Set((userForm.value.tenant_ids || []).map(Number))).sort((a, b) => a - b)
const assignmentsByTenant = new Map<number, string>()
const profileAssignmentByTenant = new Map<number, string | null>()
for (const assignment of userForm.value.role_assignments || []) {
if (!uniqueTenantIds.includes(Number(assignment.tenant_id))) continue
if (assignmentsByTenant.has(Number(assignment.tenant_id))) continue
assignmentsByTenant.set(Number(assignment.tenant_id), assignment.role_id)
}
for (const assignment of userForm.value.profile_assignments || []) {
if (!uniqueTenantIds.includes(Number(assignment.tenant_id))) continue
profileAssignmentByTenant.set(Number(assignment.tenant_id), assignment.profile_id || null)
}
userForm.value.tenant_ids = uniqueTenantIds
userForm.value.role_assignments = uniqueTenantIds
.map((tenantId) => {
const roleId = assignmentsByTenant.get(tenantId)
return roleId ? { tenant_id: tenantId, role_id: roleId } : null
})
.filter(Boolean) as { tenant_id: number; role_id: string }[]
userForm.value.profile_assignments = uniqueTenantIds.map((tenantId) => ({
tenant_id: tenantId,
profile_id: profileAssignmentByTenant.get(tenantId) || null,
}))
}
const updateUserTenants = (tenantIds: number[] = []) => {
if (!userForm.value) return
userForm.value.tenant_ids = tenantIds
normalizeUserAssignments()
}
const setRoleForTenant = (tenantId: number, roleId?: string | null) => {
if (!userForm.value) return
userForm.value.role_assignments = (userForm.value.role_assignments || []).filter((assignment) => assignment.tenant_id !== tenantId)
if (roleId) {
userForm.value.role_assignments.push({
tenant_id: tenantId,
role_id: roleId,
})
}
}
const getRoleForTenant = (tenantId: number) => {
return userForm.value?.role_assignments?.find((assignment) => assignment.tenant_id === tenantId)?.role_id || null
}
const setProfileAssignmentForTenant = (tenantId: number, profileId?: string | null) => {
if (!userForm.value) return
userForm.value.profile_assignments = (userForm.value.profile_assignments || []).filter((assignment) => assignment.tenant_id !== tenantId)
userForm.value.profile_assignments.push({
tenant_id: tenantId,
profile_id: profileId || null,
})
}
const getProfileAssignmentForTenant = (tenantId: number) => {
return userForm.value?.profile_assignments?.find((assignment) => assignment.tenant_id === tenantId)?.profile_id || null
}
const selectUser = (row: any) => {
const user = users.value.find((entry) => entry.id === row.id)
if (!user) return
selectedUserId.value = user.id
userForm.value = cloneUser(user)
normalizeUserAssignments()
}
const selectTenant = (row: any) => {
const tenant = tenants.value.find((entry) => entry.id === row.id)
if (!tenant) return
selectedTenantId.value = tenant.id
tenantForm.value = cloneTenant(tenant)
}
const fetchOverview = async () => {
loading.value = true
try {
const response = await useNuxtApp().$api("/api/admin/overview")
users.value = response.users || []
tenants.value = response.tenants || []
roles.value = response.roles || []
unassignedProfiles.value = response.unassignedProfiles || []
if (!selectedUserId.value && users.value.length) {
selectUser(users.value[0])
} else if (selectedUserId.value) {
const currentUser = users.value.find((user) => user.id === selectedUserId.value)
if (currentUser) selectUser(currentUser)
}
if (!selectedTenantId.value && tenants.value.length) {
selectTenant(tenants.value[0])
} else if (selectedTenantId.value) {
const currentTenant = tenants.value.find((tenant) => tenant.id === selectedTenantId.value)
if (currentTenant) selectTenant(currentTenant)
}
} catch (err: any) {
console.error("[admin/fetchOverview]", err)
toast.add({
title: "Administration konnte nicht geladen werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "red",
})
} finally {
loading.value = false
}
}
const saveUser = async () => {
if (!userForm.value || savingUser.value) return
savingUser.value = true
normalizeUserAssignments()
try {
await useNuxtApp().$api(`/api/admin/users/${userForm.value.id}`, {
method: "PUT",
body: {
email: userForm.value.email,
multiTenant: userForm.value.multiTenant,
must_change_password: userForm.value.must_change_password,
is_admin: userForm.value.is_admin,
},
})
await useNuxtApp().$api(`/api/admin/users/${userForm.value.id}/access`, {
method: "PUT",
body: {
tenant_ids: userForm.value.tenant_ids,
role_assignments: userForm.value.role_assignments,
profile_defaults: userForm.value.profile_defaults,
profile_assignments: userForm.value.profile_assignments,
},
})
await fetchOverview()
await auth.fetchMe()
toast.add({
title: "Benutzer gespeichert",
color: "green",
})
} catch (err: any) {
console.error("[admin/saveUser]", err)
toast.add({
title: "Benutzer konnte nicht gespeichert werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "red",
})
} finally {
savingUser.value = false
}
}
const createUser = async () => {
if (creatingUser.value) return
creatingUser.value = true
try {
const response = await useNuxtApp().$api("/api/admin/users", {
method: "POST",
body: createUserForm.value,
})
createdUserPassword.value = response.initialPassword || ""
createUserModalOpen.value = false
createUserForm.value = {
email: "",
password: "",
first_name: "",
last_name: "",
is_admin: false,
multiTenant: true,
}
await fetchOverview()
if (response.user?.id) {
const createdUser = users.value.find((user) => user.id === response.user.id)
if (createdUser) selectUser(createdUser)
}
toast.add({
title: "Benutzer angelegt",
description: createdUserPassword.value ? `Initialpasswort: ${createdUserPassword.value}` : undefined,
color: "green",
})
} catch (err: any) {
console.error("[admin/createUser]", err)
toast.add({
title: "Benutzer konnte nicht angelegt werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "red",
})
} finally {
creatingUser.value = false
}
}
const saveTenant = async () => {
if (!tenantForm.value || savingTenant.value) return
savingTenant.value = true
try {
await useNuxtApp().$api(`/api/admin/tenants/${tenantForm.value.id}`, {
method: "PUT",
body: {
name: tenantForm.value.name,
short: tenantForm.value.short,
},
})
await fetchOverview()
await auth.fetchMe()
toast.add({
title: "Tenant gespeichert",
color: "green",
})
} catch (err: any) {
console.error("[admin/saveTenant]", err)
toast.add({
title: "Tenant konnte nicht gespeichert werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "red",
})
} finally {
savingTenant.value = false
}
}
const createTenant = async () => {
if (creatingTenant.value) return
creatingTenant.value = true
try {
const response = await useNuxtApp().$api("/api/admin/tenants", {
method: "POST",
body: createTenantForm.value,
})
createTenantModalOpen.value = false
createTenantForm.value = {
name: "",
short: "",
}
await fetchOverview()
if (response.tenant?.id) {
const createdTenant = tenants.value.find((tenant) => tenant.id === response.tenant.id)
if (createdTenant) selectTenant(createdTenant)
}
toast.add({
title: "Tenant angelegt",
description: "Standardordner und Datei-Tags wurden erstellt.",
color: "green",
})
} catch (err: any) {
console.error("[admin/createTenant]", err)
toast.add({
title: "Tenant konnte nicht angelegt werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "red",
})
} finally {
creatingTenant.value = false
}
}
onMounted(async () => {
if (!auth.user?.is_admin) {
toast.add({
title: "Zugriff verweigert",
description: "Diese Seite ist nur für administrative Benutzer verfügbar.",
color: "red",
})
await router.push("/")
return
}
await fetchOverview()
})
</script>
<template>
<UDashboardNavbar title="Administration" />
<UDashboardPanelContent class="p-5 overflow-hidden">
<UAlert
v-if="!auth.user?.is_admin"
title="Kein Zugriff"
description="Für diese Seite wird ein administrativer Benutzer benötigt."
color="red"
variant="soft"
/>
<UTabs
v-else
v-model="activeTab"
:items="tabItems"
class="admin-tabs h-full"
>
<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">
<h2 class="text-lg font-semibold">Benutzer</h2>
<div class="flex items-center gap-2">
<UBadge variant="subtle">{{ users.length }}</UBadge>
<UButton
size="sm"
icon="i-heroicons-plus"
@click="createUserModalOpen = true"
>
Benutzer
</UButton>
</div>
</div>
<div class="admin-scroll">
<UTable
v-if="!loading"
:data="userTableRows"
:columns="normalizedUserTableColumns"
:on-select="selectUser"
/>
<USkeleton v-else class="h-80" />
</div>
</UCard>
<UCard class="admin-card xl:col-span-2">
<div v-if="userForm" class="admin-scroll space-y-6">
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="text-lg font-semibold">{{ userForm.display_name }}</h2>
<p class="text-sm text-gray-500">{{ userForm.email }}</p>
</div>
<UButton
color="primary"
:loading="savingUser"
@click="saveUser"
>
Benutzer speichern
</UButton>
</div>
<UForm :state="userForm" class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UFormField label="E-Mail">
<UInput v-model="userForm.email" />
</UFormField>
<UFormField label="Profil Vorname">
<UInput v-model="userForm.profile_defaults.first_name" />
</UFormField>
<UFormField label="Profil Nachname">
<UInput v-model="userForm.profile_defaults.last_name" />
</UFormField>
<UFormField label="Tenants">
<USelectMenu
:model-value="userForm.tenant_ids"
:items="tenantOptions"
value-key="value"
label-key="label"
multiple
@update:model-value="updateUserTenants"
/>
</UFormField>
<UFormField label="Administrative Freigabe">
<div class="flex items-center gap-3 h-10">
<USwitch v-model="userForm.is_admin" />
<span class="text-sm text-gray-600">Darf Administrationsseite und Admin-API nutzen</span>
</div>
</UFormField>
<UFormField label="Multi-Tenant">
<div class="flex items-center gap-3 h-10">
<USwitch v-model="userForm.multiTenant" />
<span class="text-sm text-gray-600">Benutzer darf mehreren Tenants zugeordnet sein</span>
</div>
</UFormField>
<UFormField label="Passwortwechsel erzwingen">
<div class="flex items-center gap-3 h-10">
<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>
</UFormField>
</UForm>
<div>
<USeparator label="Rollen pro Tenant" class="mb-4" />
<div
v-if="userForm.tenant_ids.length"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<UCard
v-for="tenantId in userForm.tenant_ids"
:key="tenantId"
class="border border-gray-200"
>
<div class="space-y-3">
<div>
<div class="font-medium">
{{ tenants.find((tenant) => tenant.id === tenantId)?.name || `Tenant ${tenantId}` }}
</div>
<div class="text-sm text-gray-500">
{{ tenants.find((tenant) => tenant.id === tenantId)?.short || "" }}
</div>
</div>
<UFormField label="Rolle">
<USelectMenu
:model-value="getRoleForTenant(tenantId)"
:items="getRoleOptionsForTenant(tenantId)"
value-key="value"
label-key="label"
placeholder="Rolle auswählen"
@update:model-value="(value) => setRoleForTenant(tenantId, value)"
/>
</UFormField>
<UFormField label="Freies Profil">
<USelectMenu
:model-value="getProfileAssignmentForTenant(tenantId)"
: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-key="value"
label-key="label"
placeholder="Profil auswählen"
@update:model-value="(value) => setProfileAssignmentForTenant(tenantId, value)"
/>
</UFormField>
</div>
</UCard>
</div>
<UAlert
v-else
title="Keine Tenant-Zuordnung"
description="Weise dem Benutzer zuerst mindestens einen Tenant zu."
color="amber"
variant="soft"
/>
</div>
<div>
<USeparator label="Profile im System" class="mb-4" />
<div class="flex flex-wrap gap-2">
<UBadge
v-for="profile in userForm.profiles"
:key="profile.id"
variant="subtle"
color="gray"
>
{{ profile.full_name || `${profile.first_name} ${profile.last_name}` }} · Tenant {{ profile.tenant_id }}
</UBadge>
</div>
</div>
</div>
<UAlert
v-else-if="!loading"
title="Kein Benutzer ausgewählt"
description="Wähle links einen Benutzer aus, um seine Zuordnungen zu bearbeiten."
color="gray"
variant="soft"
/>
<div v-else class="admin-scroll">
<USkeleton class="h-80" />
</div>
</UCard>
</div>
<div v-else-if="item.label === 'Tenants'" 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">
<h2 class="text-lg font-semibold">Tenants</h2>
<div class="flex items-center gap-2">
<UBadge variant="subtle">{{ tenants.length }}</UBadge>
<UButton
size="sm"
icon="i-heroicons-plus"
@click="createTenantModalOpen = true"
>
Tenant
</UButton>
</div>
</div>
<div class="admin-scroll">
<UTable
v-if="!loading"
:data="tenantTableRows"
:columns="normalizedTenantTableColumns"
:on-select="selectTenant"
/>
<USkeleton v-else class="h-80" />
</div>
</UCard>
<UCard class="admin-card xl:col-span-2">
<div v-if="tenantForm" class="admin-scroll space-y-6">
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="text-lg font-semibold">{{ tenantForm.name }}</h2>
<p class="text-sm text-gray-500">Tenant-ID {{ tenantForm.id }}</p>
</div>
<UButton
color="primary"
:loading="savingTenant"
@click="saveTenant"
>
Tenant speichern
</UButton>
</div>
<UForm :state="tenantForm" class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UFormField label="Name">
<UInput v-model="tenantForm.name" />
</UFormField>
<UFormField label="Kürzel">
<UInput v-model="tenantForm.short" />
</UFormField>
</UForm>
<div>
<USeparator label="Zugeordnete Benutzer" class="mb-4" />
<div class="flex flex-wrap gap-2">
<UBadge
v-for="user in getUsersForTenant(tenantForm.id)"
:key="`${tenantForm.id}-${user.id}`"
:color="user.is_admin ? 'primary' : 'gray'"
variant="subtle"
>
{{ user.display_name }}
</UBadge>
</div>
</div>
</div>
<UAlert
v-else-if="!loading"
title="Kein Tenant ausgewählt"
description="Wähle links einen Tenant aus, um ihn zu bearbeiten."
color="gray"
variant="soft"
/>
<div v-else class="admin-scroll">
<USkeleton class="h-80" />
</div>
</UCard>
</div>
</template>
</UTabs>
</UDashboardPanelContent>
<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"
>
<UFormField label="E-Mail">
<UInput v-model="createUserForm.email" type="email" />
</UFormField>
<UFormField label="Initialpasswort">
<UInput
v-model="createUserForm.password"
type="text"
placeholder="Leer lassen für automatisches Passwort"
/>
</UFormField>
<UFormField label="Vorname für neues Profil">
<UInput v-model="createUserForm.first_name" />
</UFormField>
<UFormField label="Nachname für neues Profil">
<UInput v-model="createUserForm.last_name" />
</UFormField>
<UFormField label="Administrative Freigabe">
<div class="flex items-center gap-3 h-10">
<USwitch v-model="createUserForm.is_admin" />
<span class="text-sm text-gray-600">Benutzer darf die Administration öffnen</span>
</div>
</UFormField>
<UFormField label="Multi-Tenant">
<div class="flex items-center gap-3 h-10">
<USwitch v-model="createUserForm.multiTenant" />
<span class="text-sm text-gray-600">Mehrere Tenant-Zuordnungen erlauben</span>
</div>
</UFormField>
<div class="flex justify-end gap-3 pt-2">
<UButton color="gray" variant="soft" @click="createUserModalOpen = false">
Abbrechen
</UButton>
<UButton type="submit" color="primary" :loading="creatingUser">
Benutzer anlegen
</UButton>
</div>
</UForm>
</UCard>
</template>
</UModal>
<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"
>
<UFormField label="Name">
<UInput v-model="createTenantForm.name" />
</UFormField>
<UFormField label="Kürzel">
<UInput v-model="createTenantForm.short" />
</UFormField>
<UAlert
title="Seed-Daten"
description="Beim Anlegen werden Standard-Datei-Tags und Systemordner für Dokumente und Eingangsbelege erzeugt."
color="primary"
variant="soft"
/>
<div class="flex justify-end gap-3 pt-2">
<UButton color="gray" variant="soft" @click="createTenantModalOpen = false">
Abbrechen
</UButton>
<UButton type="submit" color="primary" :loading="creatingTenant">
Tenant anlegen
</UButton>
</div>
</UForm>
</UCard>
</template>
</UModal>
<div class="mx-5 mb-5">
<UAlert
v-if="createdUserPassword"
title="Initialpasswort für neuen Benutzer"
:description="createdUserPassword"
color="amber"
variant="soft"
close-button
@close="createdUserPassword = ''"
/>
</div>
</template>
<style scoped>
.admin-tabs :deep(.tabs-content) {
height: 100%;
min-height: 0;
}
.admin-tabs :deep(.tab-pane) {
height: 100%;
min-height: 0;
}
.admin-grid {
height: calc(100vh - 13rem);
min-height: 0;
}
.admin-card {
height: 100%;
min-height: 0;
}
.admin-card :deep(.divide-y) {
height: 100%;
}
.admin-card :deep(.px-4.py-5.sm\:p-6) {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
.admin-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
}
</style>

View File

@@ -68,7 +68,7 @@ const addAccount = async (account) => {
} }
} catch (error) { } catch (error) {
console.error(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() setupPage()
} catch (error) { } catch (error) {
console.log(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 + Bankverbindung
</UButton> </UButton>
<USlideover <USlideover
v-model="showAddBankRequisition" v-model:open="showAddBankRequisition"
> >
<UCard <template #body>
class="h-full" <UCard
> class="h-full"
<template #header> >
<p>Bankverbindung hinzufügen</p> <template #header>
</template> <p>Bankverbindung hinzufügen</p>
</template>
<UFormGroup <UFormField
label="BIC:" label="BIC:"
class="flex-auto" class="flex-auto"
> >
@@ -127,7 +128,7 @@ setupPage()
</UButton> </UButton>
</InputGroup> </InputGroup>
</UFormGroup> </UFormField>
<UAlert <UAlert
v-if="showAlert && bankData.id && bankData.countries.includes('DE')" v-if="showAlert && bankData.id && bankData.countries.includes('DE')"
title="Bank gefunden" title="Bank gefunden"
@@ -137,51 +138,54 @@ setupPage()
class="mt-3" class="mt-3"
:actions="[{ variant: 'solid', color: 'primary', label: 'Verbinden',click: generateLink }]" :actions="[{ variant: 'solid', color: 'primary', label: 'Verbinden',click: generateLink }]"
/> />
<UAlert <UAlert
v-else-if="showAlert && !bankData.id" v-else-if="showAlert && !bankData.id"
title="Bank nicht gefunden" title="Bank nicht gefunden"
icon="i-heroicons-x-circle" icon="i-heroicons-x-circle"
color="rose" color="error"
variant="outline" variant="outline"
class="mt-3" class="mt-3"
/> />
</UCard> </UCard>
</template>
</USlideover> </USlideover>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UModal v-model="showReqData"> <UModal v-model:open="showReqData">
<UCard> <template #content>
<template #header> <UCard>
Verfügbare Bankkonten <template #header>
</template> Verfügbare Bankkonten
<div </template>
v-for="account in reqData.accounts" <div
:key="account.id" v-for="account in reqData.accounts"
class="p-2 m-3 flex justify-between" :key="account.id"
> class="p-2 m-3 flex justify-between"
{{account.iban}} - {{account.owner_name}} >
{{account.iban}} - {{account.owner_name}}
<UButton <UButton
@click="addAccount(account)" @click="addAccount(account)"
v-if="!bankaccounts.find(i => i.iban === account.iban)" v-if="!bankaccounts.find(i => i.iban === account.iban)"
> >
Hinzufügen Hinzufügen
</UButton> </UButton>
<UButton <UButton
@click="updateAccount(account)" @click="updateAccount(account)"
v-else v-else
> >
Aktualisieren Aktualisieren
</UButton> </UButton>
</div> </div>
</UCard> </UCard>
</template>
</UModal> </UModal>
<UTable <UTable
:rows="bankaccounts" :data="bankaccounts"
:columns="[ :columns="normalizeTableColumns([
{ {
key: 'expired', key: 'expired',
label: 'Aktiv' label: 'Aktiv'
@@ -198,23 +202,23 @@ setupPage()
key: 'balance', key: 'balance',
label: 'Saldo' label: 'Saldo'
}, },
]" ])"
> >
<template #expired-data="{row}"> <template #expired-cell="{ row }">
<span v-if="row.expired" class="text-rose-600">Ausgelaufen</span> <span v-if="row.original.expired" class="text-error-600">Ausgelaufen</span>
<span v-else class="text-primary">Aktiv</span> <span v-else class="text-primary">Aktiv</span>
<UButton <UButton
v-if="row.expired" v-if="row.original.expired"
variant="outline" variant="outline"
class="ml-2" class="ml-2"
@click="generateLink(row.bankId)" @click="generateLink(row.original.bankId)"
>Aktualisieren</UButton> >Aktualisieren</UButton>
</template> </template>
<template #balance-data="{row}"> <template #balance-cell="{ row }">
{{row.balance ? row.balance.toFixed(2).replace(".",",") + ' €' : '-'}} {{ row.original.balance ? row.original.balance.toFixed(2).replace(".",",") + ' €' : '-' }}
</template> </template>
<template #iban-data="{row}"> <template #iban-cell="{ row }">
{{row.iban.match(/.{1,5}/g).join(" ")}} {{ row.original.iban.match(/.{1,5}/g).join(" ") }}
</template> </template>
</UTable> </UTable>

View File

@@ -57,15 +57,15 @@ const saveAccount = async () => {
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UForm class="w-2/3 mx-auto mt-5"> <UForm class="w-2/3 mx-auto mt-5">
<UFormGroup <UFormField
label="E-Mail Adresse" label="E-Mail Adresse"
> >
<UInput <UInput
v-model="itemInfo.email" v-model="itemInfo.email"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="Passwort" label="Passwort"
> >
<UInput <UInput
@@ -73,61 +73,61 @@ const saveAccount = async () => {
v-model="itemInfo.password" v-model="itemInfo.password"
placeholder="********" placeholder="********"
/> />
</UFormGroup> </UFormField>
<UDivider> IMAP </UDivider> <USeparator label="IMAP"/>
<UFormGroup <UFormField
label="IMAP Host" label="IMAP Host"
> >
<UInput <UInput
v-model="itemInfo.imap_host" v-model="itemInfo.imap_host"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="IMAP Port" label="IMAP Port"
> >
<UInput <UInput
type="number" type="number"
v-model="itemInfo.imap_port" v-model="itemInfo.imap_port"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="IMAP SSL" label="IMAP SSL"
> >
<UToggle <USwitch
v-model="itemInfo.imap_ssl" v-model="itemInfo.imap_ssl"
/> />
</UFormGroup> </UFormField>
<UDivider> SMTP </UDivider> <USeparator label="SMTP"/>
<UFormGroup <UFormField
label="SMTP Host" label="SMTP Host"
> >
<UInput <UInput
v-model="itemInfo.smtp_host" v-model="itemInfo.smtp_host"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="SMTP Port" label="SMTP Port"
> >
<UInput <UInput
type="number" type="number"
v-model="itemInfo.smtp_port" v-model="itemInfo.smtp_port"
/> />
</UFormGroup> </UFormField>
<UFormGroup <UFormField
label="SMTP SSL" label="SMTP SSL"
> >
<UToggle <USwitch
v-model="itemInfo.smtp_ssl" v-model="itemInfo.smtp_ssl"
/> />
</UFormGroup> </UFormField>
</UForm> </UForm>
</template> </template>

Some files were not shown because too many files have changed in this diff Show More