Merge pull request 'dev' (#61) from dev into main
Reviewed-on: #61
This commit was merged in pull request #61.
This commit is contained in:
38
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
38
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Beschreibung**
|
||||
Eine klare und prägnante Beschreibung des Fehlers.
|
||||
|
||||
**Reproduktion**
|
||||
Schritte, um den Fehler zu reproduzieren:
|
||||
|
||||
Entweder:
|
||||
1. Gehe zu '...'
|
||||
2. Klicke auf '...'
|
||||
3. Scrolle runter zu '...'
|
||||
4. Siehe Fehler
|
||||
|
||||
Oder Link zur Seite
|
||||
|
||||
**Erwartetes Verhalten**
|
||||
Eine klare Beschreibung dessen, was du erwartet hast.
|
||||
|
||||
**Screenshots**
|
||||
Falls zutreffend, füge hier Screenshots oder Gifs hinzu, um das Problem zu verdeutlichen.
|
||||
|
||||
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
||||
|
||||
**Umgebung:**
|
||||
- Betriebssystem: [z.B. Windows, macOS, Linux]
|
||||
- Browser / Version (falls relevant): [z.B. Chrome 120]
|
||||
- Projekt-Version: [z.B. v1.0.2]
|
||||
|
||||
**Zusätzlicher Kontext**
|
||||
Füge hier alle anderen Informationen zum Problem hinzu.
|
||||
110
README.md
110
README.md
@@ -1 +1,109 @@
|
||||
TEST
|
||||
|
||||
|
||||
|
||||
# Docker Compose Setup
|
||||
|
||||
## ENV Vars
|
||||
|
||||
- DOMAIN
|
||||
- PDF_LICENSE
|
||||
- DB_PASS
|
||||
- DB_USER
|
||||
- CONTACT_EMAIL
|
||||
|
||||
## Docker Compose File
|
||||
~~~
|
||||
services:
|
||||
frontend:
|
||||
image: git.federspiel.tech/flfeders/fedeo/frontend:main
|
||||
restart: always
|
||||
environment:
|
||||
- NUXT_PUBLIC_API_BASE=https://${DOMAIN}/backend
|
||||
- NUXT_PUBLIC_PDF_LICENSE=${PDF_LICENSE}
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
- "traefik.port=3000"
|
||||
# Middlewares
|
||||
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
|
||||
# Web Entrypoint
|
||||
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
|
||||
- "traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
|
||||
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
|
||||
# Web Secure Entrypoint
|
||||
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
|
||||
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" #
|
||||
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
|
||||
backend:
|
||||
image: git.federspiel.tech/flfeders/fedeo/backend:main
|
||||
restart: always
|
||||
environment:
|
||||
- INFISICAL_CLIENT_ID=
|
||||
- INFISICAL_CLIENT_SECRET=
|
||||
- NODE_ENV=production
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
- "traefik.port=3100"
|
||||
# Middlewares
|
||||
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
|
||||
# Web Entrypoint
|
||||
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
|
||||
- "traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
|
||||
- "traefik.http.routers.fedeo-backend.entrypoints=web"
|
||||
# Web Secure Entrypoint
|
||||
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
|
||||
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
|
||||
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
|
||||
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
|
||||
# db:
|
||||
# image: postgres
|
||||
# restart: always
|
||||
# shm_size: 128mb
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD:
|
||||
# POSTGRES_USER:
|
||||
# POSTGRES_DB:
|
||||
# volumes:
|
||||
# - ./pg-data:/var/lib/postgresql/data
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
traefik:
|
||||
image: traefik:v2.11
|
||||
restart: unless-stopped
|
||||
container_name: traefik
|
||||
command:
|
||||
- "--api.insecure=false"
|
||||
- "--api.dashboard=false"
|
||||
- "--api.debug=false"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--providers.docker.network=traefik"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.web-secured.address=:443"
|
||||
- "--accesslog=true"
|
||||
- "--accesslog.filepath=/logs/access.log"
|
||||
- "--accesslog.bufferingsize=5000"
|
||||
- "--accesslog.fields.defaultMode=keep"
|
||||
- "--accesslog.fields.headers.defaultMode=keep"
|
||||
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" #
|
||||
- "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}"
|
||||
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS)
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "./traefik/logs:/logs"
|
||||
networks:
|
||||
- traefik
|
||||
networks:
|
||||
traefik:
|
||||
external: false
|
||||
~~~
|
||||
@@ -71,7 +71,7 @@ export const projects = pgTable("projects", {
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
active_phase: text("active_phase"),
|
||||
active_phase: text("active_phase").default("Erstkontakt"),
|
||||
})
|
||||
|
||||
export type Project = typeof projects.$inferSelect
|
||||
|
||||
@@ -11,58 +11,64 @@ import {secrets} from "../utils/secrets";
|
||||
import {createSEPAExport} from "../utils/export/sepa";
|
||||
|
||||
const createDatevExport = async (server:FastifyInstance,req:any,startDate,endDate,beraternr,mandantennr) => {
|
||||
console.log(startDate,endDate,beraternr,mandantennr)
|
||||
try {
|
||||
console.log(startDate,endDate,beraternr,mandantennr)
|
||||
|
||||
// 1) ZIP erzeugen
|
||||
const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr)
|
||||
console.log("ZIP created")
|
||||
console.log(buffer)
|
||||
// 1) ZIP erzeugen
|
||||
const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr)
|
||||
console.log("ZIP created")
|
||||
console.log(buffer)
|
||||
|
||||
// 2) Dateiname & Key festlegen
|
||||
const fileKey = `${req.user.tenant_id}/exports/Export_${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}_${randomUUID()}.zip`
|
||||
console.log(fileKey)
|
||||
// 2) Dateiname & Key festlegen
|
||||
const fileKey = `${req.user.tenant_id}/exports/Export_${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}_${randomUUID()}.zip`
|
||||
console.log(fileKey)
|
||||
|
||||
// 3) In S3 hochladen
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: fileKey,
|
||||
Body: buffer,
|
||||
ContentType: "application/zip",
|
||||
})
|
||||
)
|
||||
// 3) In S3 hochladen
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: fileKey,
|
||||
Body: buffer,
|
||||
ContentType: "application/zip",
|
||||
})
|
||||
)
|
||||
|
||||
// 4) Presigned URL erzeugen (24h gültig)
|
||||
const url = await getSignedUrl(
|
||||
s3,
|
||||
new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: fileKey,
|
||||
}),
|
||||
{ expiresIn: 60 * 60 * 24 }
|
||||
)
|
||||
// 4) Presigned URL erzeugen (24h gültig)
|
||||
const url = await getSignedUrl(
|
||||
s3,
|
||||
new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: fileKey,
|
||||
}),
|
||||
{ expiresIn: 60 * 60 * 24 }
|
||||
)
|
||||
|
||||
console.log(url)
|
||||
console.log(url)
|
||||
|
||||
// 5) In Supabase-DB speichern
|
||||
const { data, error } = await server.supabase
|
||||
.from("exports")
|
||||
.insert([
|
||||
{
|
||||
tenant_id: req.user.tenant_id,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
valid_until: dayjs().add(24,"hours").toISOString(),
|
||||
file_path: fileKey,
|
||||
url: url,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
])
|
||||
.select()
|
||||
.single()
|
||||
|
||||
console.log(data)
|
||||
console.log(error)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
// 5) In Supabase-DB speichern
|
||||
const { data, error } = await server.supabase
|
||||
.from("exports")
|
||||
.insert([
|
||||
{
|
||||
tenant_id: req.user.tenant_id,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
valid_until: dayjs().add(24,"hours").toISOString(),
|
||||
file_path: fileKey,
|
||||
url: url,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
])
|
||||
.select()
|
||||
.single()
|
||||
|
||||
console.log(data)
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -461,10 +461,9 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
}
|
||||
|
||||
Object.keys(createData).forEach((key) => {
|
||||
if(key.toLowerCase().includes("date")) createData[key] = normalizeDate(createData[key])
|
||||
if(key.toLowerCase().includes("date") && key !== "deliveryDateType") createData[key] = normalizeDate(createData[key])
|
||||
})
|
||||
|
||||
|
||||
const [created] = await server.db
|
||||
.insert(table)
|
||||
.values(createData)
|
||||
@@ -513,8 +512,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
let data = {...body, updated_at: new Date().toISOString(), updated_by: userId}
|
||||
|
||||
//@ts-ignore
|
||||
delete data.updatedBy
|
||||
//@ts-ignore
|
||||
delete data.updatedAt
|
||||
|
||||
console.log(data)
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
if(key.includes("_at") || key.includes("At")) {
|
||||
console.log(key)
|
||||
|
||||
if(key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) {
|
||||
data[key] = normalizeDate(data[key])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
|
||||
|
||||
server.post("/staff/time/event", async (req, reply) => {
|
||||
try {
|
||||
const userId = req.user.user_id
|
||||
const actorId = req.user.user_id;
|
||||
const tenantId = req.user.tenant_id
|
||||
|
||||
const body = req.body as any
|
||||
@@ -35,17 +35,15 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
|
||||
|
||||
const dataToInsert = {
|
||||
tenant_id: tenantId,
|
||||
user_id: userId,
|
||||
user_id: body.user_id,
|
||||
actortype: "user",
|
||||
actoruser_id: userId,
|
||||
actoruser_id: actorId,
|
||||
eventtime: normalizeDate(body.eventtime),
|
||||
eventtype: body.eventtype,
|
||||
source: "WEB",
|
||||
payload: body.payload // Payload (z.B. Description) mit speichern
|
||||
}
|
||||
|
||||
console.log(dataToInsert)
|
||||
|
||||
const [created] = await server.db
|
||||
.insert(stafftimeevents)
|
||||
//@ts-ignore
|
||||
@@ -390,7 +388,9 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
|
||||
const evaluatedUserId = targetUserId || actingUserId;
|
||||
|
||||
const startDate = new Date(from);
|
||||
const endDate = new Date(to);
|
||||
let endDateQuery = new Date(to);
|
||||
endDateQuery.setDate(endDateQuery.getDate() + 1);
|
||||
const endDate = endDateQuery;
|
||||
|
||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||
return reply.code(400).send({ error: "Ungültiges Datumsformat." });
|
||||
|
||||
@@ -1,327 +1,389 @@
|
||||
import xmlbuilder from "xmlbuilder";
|
||||
import dayjs from "dayjs";
|
||||
import isBetween from "dayjs/plugin/isBetween.js"
|
||||
import {BlobWriter, Data64URIReader, TextReader, TextWriter, ZipWriter} from "@zip.js/zip.js";
|
||||
import {FastifyInstance} from "fastify";
|
||||
import {GetObjectCommand} from "@aws-sdk/client-s3";
|
||||
import {s3} from "../s3";
|
||||
import {secrets} from "../secrets";
|
||||
dayjs.extend(isBetween)
|
||||
import isBetween from "dayjs/plugin/isBetween.js";
|
||||
import { BlobWriter, Data64URIReader, TextReader, ZipWriter } from "@zip.js/zip.js";
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { s3 } from "../s3";
|
||||
import { secrets } from "../secrets";
|
||||
|
||||
const getCreatedDocumentTotal = (item) => {
|
||||
let totalNet = 0
|
||||
let total19 = 0
|
||||
let total7 = 0
|
||||
// Drizzle Core Imports
|
||||
import { eq, and, inArray, gte, lte, asc, aliasedTable } from "drizzle-orm";
|
||||
|
||||
item.rows.forEach(row => {
|
||||
if(!['pagebreak','title','text'].includes(row.mode)){
|
||||
let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(3)
|
||||
totalNet = totalNet + Number(rowPrice)
|
||||
// Tabellen Imports (keine Relations nötig!)
|
||||
import {
|
||||
statementallocations,
|
||||
createddocuments,
|
||||
incominginvoices,
|
||||
accounts,
|
||||
files,
|
||||
customers,
|
||||
vendors,
|
||||
bankaccounts,
|
||||
bankstatements,
|
||||
ownaccounts
|
||||
} from "../../../db/schema";
|
||||
|
||||
if(row.taxPercent === 19) {
|
||||
// @ts-ignore
|
||||
total19 = total19 + Number(rowPrice * 0.19)
|
||||
} else if(row.taxPercent === 7) {
|
||||
// @ts-ignore
|
||||
total7 = total7 + Number(rowPrice * 0.07)
|
||||
}
|
||||
dayjs.extend(isBetween);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// HELPER FUNCTIONS (Unverändert)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
const getCreatedDocumentTotal = (item: any) => {
|
||||
let totalNet = 0;
|
||||
let total19:number = 0;
|
||||
let total7:number = 0;
|
||||
const rows = Array.isArray(item.rows) ? item.rows : [];
|
||||
rows.forEach((row: any) => {
|
||||
if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
|
||||
let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100)).toFixed(3);
|
||||
totalNet = totalNet + Number(rowPrice);
|
||||
if (row.taxPercent === 19) total19 += Number(rowPrice) * Number(0.19);
|
||||
else if (row.taxPercent === 7) total7 += Number(rowPrice) * Number(0.07);
|
||||
}
|
||||
})
|
||||
|
||||
let totalGross = Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
|
||||
|
||||
|
||||
|
||||
});
|
||||
return {
|
||||
totalNet: totalNet,
|
||||
total19: total19,
|
||||
total7: total7,
|
||||
totalGross: totalGross,
|
||||
}
|
||||
}
|
||||
totalNet, total19, total7,
|
||||
totalGross: Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
|
||||
};
|
||||
};
|
||||
|
||||
const escapeString = (str) => {
|
||||
const escapeString = (str: string | null | undefined) => {
|
||||
return (str || "").replaceAll("\n", "").replaceAll(";", "").replaceAll(/\r/g, "").replaceAll(/"/g, "").replaceAll(/ü/g, "ue").replaceAll(/ä/g, "ae").replaceAll(/ö/g, "oe");
|
||||
};
|
||||
|
||||
str = (str ||"")
|
||||
.replaceAll("\n","")
|
||||
.replaceAll(";","")
|
||||
.replaceAll(/\r/g,"")
|
||||
.replaceAll(/"/g,"")
|
||||
.replaceAll(/ü/g,"ue")
|
||||
.replaceAll(/ä/g,"ae")
|
||||
.replaceAll(/ö/g,"oe")
|
||||
return str
|
||||
}
|
||||
const displayCurrency = (input: number, onlyAbs = false) => {
|
||||
return (onlyAbs ? Math.abs(input) : input).toFixed(2).replace(".", ",");
|
||||
};
|
||||
|
||||
const displayCurrency = (input, onlyAbs = false) => {
|
||||
// ---------------------------------------------------------
|
||||
// MAIN EXPORT FUNCTION
|
||||
// ---------------------------------------------------------
|
||||
|
||||
if(onlyAbs) {
|
||||
return Math.abs(input).toFixed(2).replace(".",",")
|
||||
} else {
|
||||
return input.toFixed(2).replace(".",",")
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildExportZip(server: FastifyInstance, tenant: number, startDate: string, endDate: string, beraternr: string, mandantennr: string): Promise<Buffer> {
|
||||
export async function buildExportZip(
|
||||
server: FastifyInstance,
|
||||
tenantId: number,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
beraternr: string,
|
||||
mandantennr: string
|
||||
): Promise<Buffer> {
|
||||
|
||||
try {
|
||||
const zipFileWriter = new BlobWriter()
|
||||
const zipWriter = new ZipWriter(zipFileWriter)
|
||||
const zipFileWriter = new BlobWriter();
|
||||
const zipWriter = new ZipWriter(zipFileWriter);
|
||||
|
||||
// Header Infos
|
||||
const dateNowStr = dayjs().format("YYYYMMDDHHmmssSSS");
|
||||
const startDateFmt = dayjs(startDate).format("YYYYMMDD");
|
||||
const endDateFmt = dayjs(endDate).format("YYYYMMDD");
|
||||
|
||||
let header = `"EXTF";700;21;"Buchungsstapel";13;${dateNowStr};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${startDateFmt};${endDateFmt};"Buchungsstapel";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`;
|
||||
let colHeaders = `Umsatz;Soll-/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basisumsatz;WKZ Basisumsatz;Konto;Gegenkonto;BU-Schluessel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschaeftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost Menge;EU-Land u. USt-IdNr. (Bestimmung);EU-Steuersatz (Bestimmung);Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergaenzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergaenzung;Zusatzinformation - Art 1;Zusatzinformation - Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation - Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation - Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation - Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation - Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation - Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation - Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation - Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation - Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation - Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation - Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation - Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation - Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation - Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation - Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation - Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation - Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation - Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation - Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation - Inhalt 20;Stueck;Gewicht;Zahlweise;Zahlweise;Veranlagungsjahr;Zugeordnete Faelligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-Schluessel (Anzahlungen);EU-Mitgliedstaat (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erloeskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode;Faelligkeit;Generalumkehr;Steuersatz;Land;Abrechnungsreferenz;BVV-Position;EU-Mitgliedstaat u. UStID(Ursprung);EU-Steuersatz(Ursprung);Abw. Skontokonto`;
|
||||
|
||||
//Basic Information
|
||||
// ---------------------------------------------------------
|
||||
// 1. DATEN LADEN (CORE API SELECT & JOIN)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
let header = `"EXTF";700;21;"Buchungsstapel";13;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Buchungsstapel";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
|
||||
// --- A) Created Documents ---
|
||||
// Wir brauchen das Dokument und den Kunden dazu
|
||||
const cdRaw = await server.db.select({
|
||||
doc: createddocuments,
|
||||
customer: customers
|
||||
})
|
||||
.from(createddocuments)
|
||||
.leftJoin(customers, eq(createddocuments.customer, customers.id))
|
||||
.where(and(
|
||||
eq(createddocuments.tenant, tenantId),
|
||||
inArray(createddocuments.type, ["invoices", "advanceInvoices", "cancellationInvoices"]),
|
||||
eq(createddocuments.state, "Gebucht"),
|
||||
eq(createddocuments.archived, false),
|
||||
gte(createddocuments.documentDate, startDate),
|
||||
lte(createddocuments.documentDate, endDate)
|
||||
));
|
||||
|
||||
let colHeaders = `Umsatz;Soll-/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basisumsatz;WKZ Basisumsatz;Konto;Gegenkonto;BU-Schluessel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschaeftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost Menge;EU-Land u. USt-IdNr. (Bestimmung);EU-Steuersatz (Bestimmung);Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergaenzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergaenzung;Zusatzinformation - Art 1;Zusatzinformation - Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation - Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation - Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation - Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation - Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation - Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation - Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation - Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation - Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation - Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation - Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation - Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation - Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation - Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation - Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation - Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation - Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation - Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation - Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation - Inhalt 20;Stueck;Gewicht;Zahlweise;Zahlweise;Veranlagungsjahr;Zugeordnete Faelligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-Schluessel (Anzahlungen);EU-Mitgliedstaat (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erloeskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode;Faelligkeit;Generalumkehr;Steuersatz;Land;Abrechnungsreferenz;BVV-Position;EU-Mitgliedstaat u. UStID(Ursprung);EU-Steuersatz(Ursprung);Abw. Skontokonto`
|
||||
// Mapping: Flat Result -> Nested Object (damit der Rest des Codes gleich bleiben kann)
|
||||
const createddocumentsList = cdRaw.map(r => ({
|
||||
...r.doc,
|
||||
customer: r.customer
|
||||
}));
|
||||
|
||||
//Get Bookings
|
||||
const {data:statementallocationsRaw,error: statementallocationsError} = await server.supabase.from("statementallocations").select('*, account(*), bs_id(*, account(*)), cd_id(*,customer(*)), ii_id(*, vendor(*)), vendor(*), customer(*), ownaccount(*)').eq("tenant", tenant);
|
||||
let {data:createddocumentsRaw,error: createddocumentsError} = await server.supabase.from("createddocuments").select('*,customer(*)').eq("tenant", tenant).in("type",["invoices","advanceInvoices","cancellationInvoices"]).eq("state","Gebucht").eq("archived",false)
|
||||
let {data:incominginvoicesRaw,error: incominginvoicesError} = await server.supabase.from("incominginvoices").select('*, vendor(*)').eq("tenant", tenant).eq("state","Gebucht").eq("archived",false)
|
||||
const {data:accounts} = await server.supabase.from("accounts").select()
|
||||
const {data:tenantData} = await server.supabase.from("tenants").select().eq("id",tenant).single()
|
||||
// --- B) Incoming Invoices ---
|
||||
// Wir brauchen die Rechnung und den Lieferanten
|
||||
const iiRaw = await server.db.select({
|
||||
inv: incominginvoices,
|
||||
vendor: vendors
|
||||
})
|
||||
.from(incominginvoices)
|
||||
.leftJoin(vendors, eq(incominginvoices.vendor, vendors.id))
|
||||
.where(and(
|
||||
eq(incominginvoices.tenant, tenantId),
|
||||
eq(incominginvoices.state, "Gebucht"),
|
||||
eq(incominginvoices.archived, false),
|
||||
gte(incominginvoices.date, startDate),
|
||||
lte(incominginvoices.date, endDate)
|
||||
));
|
||||
|
||||
let createddocuments = createddocumentsRaw.filter(i => dayjs(i.documentDate).isBetween(startDate,endDate,"day","[]"))
|
||||
let incominginvoices = incominginvoicesRaw.filter(i => dayjs(i.date).isBetween(startDate,endDate,"day","[]"))
|
||||
let statementallocations = statementallocationsRaw.filter(i => dayjs(i.bs_id.date).isBetween(startDate,endDate,"day","[]"))
|
||||
const incominginvoicesList = iiRaw.map(r => ({
|
||||
...r.inv,
|
||||
vendor: r.vendor
|
||||
}));
|
||||
|
||||
// --- C) Statement Allocations ---
|
||||
// Das ist der komplexeste Teil. Wir müssen Tabellen aliasen, da wir z.B. Customers doppelt joinen könnten
|
||||
// (Einmal via CreatedDocument, einmal direkt an der Allocation).
|
||||
|
||||
const {data:filesCreateddocuments, error: filesErrorCD} = await server.supabase.from("files").select().eq("tenant",tenant).or(`createddocument.in.(${createddocuments.map(i => i.id).join(",")})`)
|
||||
const {data:filesIncomingInvoices, error: filesErrorII} = await server.supabase.from("files").select().eq("tenant",tenant).or(`incominginvoice.in.(${incominginvoices.map(i => i.id).join(",")})`)
|
||||
const CdCustomer = aliasedTable(customers, "cd_customer");
|
||||
const IiVendor = aliasedTable(vendors, "ii_vendor");
|
||||
|
||||
const downloadFile = async (bucketName, filePath, downloadFilePath,fileId) => {
|
||||
const allocRaw = await server.db.select({
|
||||
allocation: statementallocations,
|
||||
bs: bankstatements,
|
||||
ba: bankaccounts,
|
||||
cd: createddocuments,
|
||||
cd_cust: CdCustomer,
|
||||
ii: incominginvoices,
|
||||
ii_vend: IiVendor,
|
||||
acc: accounts,
|
||||
direct_vend: vendors, // Direkte Zuordnung an Kreditor
|
||||
direct_cust: customers, // Direkte Zuordnung an Debitor
|
||||
own: ownaccounts
|
||||
})
|
||||
.from(statementallocations)
|
||||
// JOIN 1: Bankstatement (Pflicht, für Datum Filter)
|
||||
.innerJoin(bankstatements, eq(statementallocations.bankstatement, bankstatements.id))
|
||||
// JOIN 2: Bankaccount (für DATEV Nummer)
|
||||
.leftJoin(bankaccounts, eq(bankstatements.account, bankaccounts.id))
|
||||
|
||||
console.log(filePath)
|
||||
// JOIN 3: Ausgangsrechnung & deren Kunde
|
||||
.leftJoin(createddocuments, eq(statementallocations.createddocument, createddocuments.id))
|
||||
.leftJoin(CdCustomer, eq(createddocuments.customer, CdCustomer.id))
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: filePath,
|
||||
})
|
||||
// JOIN 4: Eingangsrechnung & deren Lieferant
|
||||
.leftJoin(incominginvoices, eq(statementallocations.incominginvoice, incominginvoices.id))
|
||||
.leftJoin(IiVendor, eq(incominginvoices.vendor, IiVendor.id))
|
||||
|
||||
const { Body, ContentType } = await s3.send(command)
|
||||
// JOIN 5: Direkte Zuordnungen
|
||||
.leftJoin(accounts, eq(statementallocations.account, accounts.id))
|
||||
.leftJoin(vendors, eq(statementallocations.vendor, vendors.id))
|
||||
.leftJoin(customers, eq(statementallocations.customer, customers.id))
|
||||
.leftJoin(ownaccounts, eq(statementallocations.ownaccount, ownaccounts.id))
|
||||
|
||||
const chunks: any[] = []
|
||||
// @ts-ignore
|
||||
for await (const chunk of Body) {
|
||||
chunks.push(chunk)
|
||||
.where(and(
|
||||
eq(statementallocations.tenant, tenantId),
|
||||
eq(statementallocations.archived, false),
|
||||
// Datum Filter direkt auf dem Bankstatement
|
||||
gte(bankstatements.date, startDate),
|
||||
lte(bankstatements.date, endDate)
|
||||
));
|
||||
|
||||
// Mapping: Wir bauen das komplexe Objekt nach, das die CSV Logik erwartet
|
||||
const statementallocationsList = allocRaw.map(r => ({
|
||||
...r.allocation,
|
||||
bankstatement: {
|
||||
...r.bs,
|
||||
account: r.ba // Nesting für bs.account.datevNumber
|
||||
},
|
||||
createddocument: r.cd ? {
|
||||
...r.cd,
|
||||
customer: r.cd_cust
|
||||
} : null,
|
||||
incominginvoice: r.ii ? {
|
||||
...r.ii,
|
||||
vendor: r.ii_vend
|
||||
} : null,
|
||||
account: r.acc,
|
||||
vendor: r.direct_vend,
|
||||
customer: r.direct_cust,
|
||||
ownaccount: r.own
|
||||
}));
|
||||
|
||||
// --- D) Stammdaten Accounts ---
|
||||
const accountsList = await server.db.select().from(accounts);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 2. FILES LADEN
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// IDs sammeln für IN (...) Abfragen
|
||||
const cdIds = createddocumentsList.map(i => i.id);
|
||||
const iiIds = incominginvoicesList.map(i => i.id);
|
||||
|
||||
let filesCreateddocuments: any[] = [];
|
||||
if (cdIds.length > 0) {
|
||||
filesCreateddocuments = await server.db.select().from(files).where(and(
|
||||
eq(files.tenant, tenantId),
|
||||
inArray(files.createddocument, cdIds),
|
||||
eq(files.archived, false)
|
||||
));
|
||||
}
|
||||
|
||||
let filesIncomingInvoices: any[] = [];
|
||||
if (iiIds.length > 0) {
|
||||
filesIncomingInvoices = await server.db.select().from(files).where(and(
|
||||
eq(files.tenant, tenantId),
|
||||
inArray(files.incominginvoice, iiIds),
|
||||
eq(files.archived, false)
|
||||
));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 3. DOWNLOAD & ZIP
|
||||
// ---------------------------------------------------------
|
||||
|
||||
const downloadFile = async (filePath: string, fileId: string) => {
|
||||
try {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: filePath,
|
||||
});
|
||||
const { Body } = await s3.send(command);
|
||||
if (!Body) return;
|
||||
const chunks: any[] = [];
|
||||
// @ts-ignore
|
||||
for await (const chunk of Body) chunks.push(chunk);
|
||||
const buffer = Buffer.concat(chunks);
|
||||
const dataURL = `data:application/pdf;base64,${buffer.toString('base64')}`;
|
||||
const dataURLReader = new Data64URIReader(dataURL);
|
||||
const ext = filePath.includes('.') ? filePath.split(".").pop() : "pdf";
|
||||
await zipWriter.add(`${fileId}.${ext}`, dataURLReader);
|
||||
} catch (e) {
|
||||
console.error(`Error downloading file ${fileId}`, e);
|
||||
}
|
||||
const buffer = Buffer.concat(chunks)
|
||||
|
||||
const dataURL = `data:application/pdf;base64,${buffer.toString('base64')}`
|
||||
|
||||
const dataURLReader = new Data64URIReader(dataURL)
|
||||
await zipWriter.add(`${fileId}.${downloadFilePath.split(".").pop()}`, dataURLReader)
|
||||
|
||||
//await fs.writeFile(`./output/${fileId}.${downloadFilePath.split(".").pop()}`, buffer, () => {});
|
||||
console.log(`File added to Zip`);
|
||||
};
|
||||
|
||||
for (const file of filesCreateddocuments) {
|
||||
await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id);
|
||||
}
|
||||
for (const file of filesIncomingInvoices) {
|
||||
await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id);
|
||||
}
|
||||
for (const file of filesCreateddocuments) if(file.path) await downloadFile(file.path, file.id);
|
||||
for (const file of filesIncomingInvoices) if(file.path) await downloadFile(file.path, file.id);
|
||||
|
||||
let bookingLines = []
|
||||
// ---------------------------------------------------------
|
||||
// 4. CSV GENERIERUNG (Logic ist gleich geblieben)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
createddocuments.forEach(createddocument => {
|
||||
let bookingLines: string[] = [];
|
||||
|
||||
let file = filesCreateddocuments.find(i => i.createddocument === createddocument.id);
|
||||
// AR
|
||||
createddocumentsList.forEach(cd => {
|
||||
let file = filesCreateddocuments.find(i => i.createddocument === cd.id);
|
||||
let total = 0;
|
||||
let typeString = "";
|
||||
|
||||
let total = 0
|
||||
let typeString = ""
|
||||
|
||||
if(createddocument.type === "invoices") {
|
||||
total = getCreatedDocumentTotal(createddocument).totalGross
|
||||
|
||||
console.log()
|
||||
if(createddocument.usedAdvanceInvoices.length > 0){
|
||||
createddocument.usedAdvanceInvoices.forEach(usedAdvanceInvoice => {
|
||||
total -= getCreatedDocumentTotal(createddocumentsRaw.find(i => i.id === usedAdvanceInvoice)).totalGross
|
||||
})
|
||||
}
|
||||
|
||||
console.log(total)
|
||||
|
||||
typeString = "AR"
|
||||
} else if(createddocument.type === "advanceInvoices") {
|
||||
total = getCreatedDocumentTotal(createddocument).totalGross
|
||||
typeString = "ARAbschlag"
|
||||
} else if(createddocument.type === "cancellationInvoices") {
|
||||
total = getCreatedDocumentTotal(createddocument).totalGross
|
||||
typeString = "ARStorno"
|
||||
if(cd.type === "invoices") {
|
||||
total = getCreatedDocumentTotal(cd).totalGross;
|
||||
typeString = "AR";
|
||||
} else if(cd.type === "advanceInvoices") {
|
||||
total = getCreatedDocumentTotal(cd).totalGross;
|
||||
typeString = "ARAbschlag";
|
||||
} else if(cd.type === "cancellationInvoices") {
|
||||
total = getCreatedDocumentTotal(cd).totalGross;
|
||||
typeString = "ARStorno";
|
||||
}
|
||||
|
||||
let shSelector = "S"
|
||||
if(Math.sign(total) === 1) {
|
||||
shSelector = "S"
|
||||
} else if (Math.sign(total) === -1) {
|
||||
shSelector = "H"
|
||||
let shSelector = Math.sign(total) === -1 ? "H" : "S";
|
||||
const cust = cd.customer; // durch Mapping verfügbar
|
||||
|
||||
bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${cust?.customerNumber || ""};8400;"";${dayjs(cd.documentDate).format("DDMM")};"${cd.documentNumber}";;;"${`${typeString} ${cd.documentNumber} - ${cust?.name || ""}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${cust?.name || ""}";"Kundennummer";"${cust?.customerNumber || ""}";"Belegnummer";"${cd.documentNumber}";"Leistungsdatum";"${dayjs(cd.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(cd.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
});
|
||||
|
||||
// ER
|
||||
incominginvoicesList.forEach(ii => {
|
||||
const accs = ii.accounts as any[] || [];
|
||||
accs.forEach(account => {
|
||||
let file = filesIncomingInvoices.find(i => i.incominginvoice === ii.id);
|
||||
let accountData = accountsList.find(i => i.id === account.account);
|
||||
if (!accountData) return;
|
||||
|
||||
let buschluessel = "9";
|
||||
if(account.taxType === '19') buschluessel = "9";
|
||||
else if(account.taxType === 'null') buschluessel = "";
|
||||
else if(account.taxType === '7') buschluessel = "8";
|
||||
else if(account.taxType === '19I') buschluessel = "19";
|
||||
else if(account.taxType === '7I') buschluessel = "18";
|
||||
else buschluessel = "-";
|
||||
|
||||
let amountGross = account.amountGross ? account.amountGross : (account.amountNet || 0) + (account.amountTax || 0);
|
||||
let shSelector = Math.sign(amountGross) === -1 ? "H" : "S";
|
||||
let text = `ER ${ii.reference}: ${escapeString(ii.description)}`.substring(0,59);
|
||||
const vend = ii.vendor; // durch Mapping verfügbar
|
||||
|
||||
bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${vend?.vendorNumber || ""};"${buschluessel}";${dayjs(ii.date).format("DDMM")};"${ii.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${vend?.name || ""}";"Kundennummer";"${vend?.vendorNumber || ""}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
});
|
||||
});
|
||||
|
||||
// Bank
|
||||
statementallocationsList.forEach(alloc => {
|
||||
const bs = alloc.bankstatement; // durch Mapping verfügbar
|
||||
if(!bs) return;
|
||||
|
||||
let shSelector = Math.sign(alloc.amount) === -1 ? "H" : "S";
|
||||
// @ts-ignore
|
||||
let datevKonto = bs.account?.datevNumber || "";
|
||||
let dateVal = dayjs(bs.date).format("DDMM");
|
||||
let dateFull = dayjs(bs.date).format("DD.MM.YYYY");
|
||||
let bsText = escapeString(bs.text);
|
||||
|
||||
if(alloc.createddocument && alloc.createddocument.customer) {
|
||||
const cd = alloc.createddocument;
|
||||
const cust = cd.customer;
|
||||
bookingLines.push(`${displayCurrency(alloc.amount,true)};"H";;;;;${cust?.customerNumber};${datevKonto};"3";${dayjs(cd.documentDate).format("DDMM")};"${cd.documentNumber}";;;"${`ZE${alloc.description}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${cust?.name}";"Kundennummer";"${cust?.customerNumber}";"Belegnummer";"${cd.documentNumber}";"Leistungsdatum";"${dayjs(cd.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(cd.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
} else if(alloc.incominginvoice && alloc.incominginvoice.vendor) {
|
||||
const ii = alloc.incominginvoice;
|
||||
const vend = ii.vendor;
|
||||
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${vend?.vendorNumber};"";${dayjs(ii.date).format("DDMM")};"${ii.reference}";;;"${`ZA${alloc.description} ${bsText} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${vend?.name}";"Kundennummer";"${vend?.vendorNumber}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
} else if(alloc.account) {
|
||||
const acc = alloc.account;
|
||||
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
|
||||
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${acc.number};"";${dateVal};"";;;"${`${vorzeichen} ${acc.number} - ${escapeString(acc.label)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${bs.credName || ''}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
} else if(alloc.vendor) {
|
||||
const vend = alloc.vendor;
|
||||
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
|
||||
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${vend.vendorNumber};"";${dateVal};"";;;"${`${vorzeichen} ${vend.vendorNumber} - ${escapeString(vend.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${vend.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
} else if(alloc.customer) {
|
||||
const cust = alloc.customer;
|
||||
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
|
||||
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${cust.customerNumber};"";${dateVal};"";;;"${`${vorzeichen} ${cust.customerNumber} - ${escapeString(cust.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${cust.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
} else if(alloc.ownaccount) {
|
||||
const own = alloc.ownaccount;
|
||||
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
|
||||
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${own.number};"";${dateVal};"";;;"${`${vorzeichen} ${own.number} - ${escapeString(own.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${own.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
}
|
||||
});
|
||||
|
||||
bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${createddocument.customer.customerNumber};8400;"";${dayjs(createddocument.documentDate).format("DDMM")};"${createddocument.documentNumber}";;;"${`${typeString} ${createddocument.documentNumber} - ${createddocument.customer.name}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${createddocument.customer.name}";"Kundennummer";"${createddocument.customer.customerNumber}";"Belegnummer";"${createddocument.documentNumber}";"Leistungsdatum";"${dayjs(createddocument.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(createddocument.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
// ---------------------------------------------------------
|
||||
// 5. STAMMDATEN CSV
|
||||
// ---------------------------------------------------------
|
||||
const csvString = `${header}\n${colHeaders}\n` + bookingLines.join("\n") + "\n";
|
||||
await zipWriter.add(
|
||||
`EXTF_Buchungsstapel_von_${startDateFmt}_bis_${endDateFmt}.csv`,
|
||||
new TextReader(csvString)
|
||||
);
|
||||
|
||||
})
|
||||
const headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dateNowStr};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${startDateFmt};${endDateFmt};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`;
|
||||
const colHeadersStammdaten = `Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natuerl. Person);Vorname (Adressattyp natuerl. Person);Name (Adressattyp keine Angabe);Adressatentyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Strasse;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gueltig von;Adresse Gueltig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bankkonto-Nummer 1;Laenderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Haupt-Bankverb. 1;Bankverb. 1 Gueltig von;Bankverb. 1 Gueltig bis;Bankleitzahl 2;Bankbezeichnung 2;Bankkonto-Nummer 2;Laenderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Haupt-Bankverb. 2;Bankverb. 2 gueltig von;Bankverb. 2 gueltig bis;Bankleitzahl 3;Bankbezeichnung 3;Bankkonto-Nummer 3;Laenderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Haupt-Bankverb. 3;Bankverb. 3 gueltig von;Bankverb. 3 gueltig bis;Bankleitzahl 4;Bankbezeichnung 4;Bankkonto-Nummer 4;Laenderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Haupt-Bankverb. 4;Bankverb. 4 gueltig von;Bankverb. 4 gueltig bis;Bankleitzahl 5;Bankbezeichnung 5;Bankkonto-Nummer 5;Laenderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Haupt-Bankverb. 5;Bankverb. 5 gueltig von;Bankverb. 5 gueltig bis;Leerfeld;Briefanrede;Grussformel;Kunden-/Lief.-Nr.;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Waehrungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Faelligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungstraeger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Strasse (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gueltig von (Rechnungsadresse);Adresse Gueltig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bankkonto-Nummer 6;Laenderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Haupt-Bankverb. 6;Bankverb. 6 gueltig von;Bankverb. 6 gueltig bis;Bankleitzahl 7;Bankbezeichnung 7;Bankkonto-Nummer 7;Laenderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Haupt-Bankverb. 7;Bankverb. 7 gueltig von;Bankverb. 7 gueltig bis;Bankleitzahl 8;Bankbezeichnung 8;Bankkonto-Nummer 8;Laenderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Haupt-Bankverb. 8;Bankverb. 8 gueltig von;Bankverb. 8 gueltig bis;Bankleitzahl 9;Bankbezeichnung 9;Bankkonto-Nummer 9;Laenderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Haupt-Bankverb. 9;Bankverb. 9 gueltig von;Bankverb. 9 gueltig bis;Bankleitzahl 10;Bankbezeichnung 10;Bankkonto-Nummer 10;Laenderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Haupt-Bankverb. 10;Bankverb 10 Gueltig von;Bankverb 10 Gueltig bis;Nummer Fremdsystem;Insolvent;SEPA-Mandatsreferenz 1;SEPA-Mandatsreferenz 2;SEPA-Mandatsreferenz 3;SEPA-Mandatsreferenz 4;SEPA-Mandatsreferenz 5;SEPA-Mandatsreferenz 6;SEPA-Mandatsreferenz 7;SEPA-Mandatsreferenz 8;SEPA-Mandatsreferenz 9;SEPA-Mandatsreferenz 10;Verknuepftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebuehrenberechnung;Mahngebuehr 1;Mahngebuehr 2;Mahngebuehr 3;Pauschalberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3;Alternativer Suchname;Status;Anschrift manuell geaendert (Korrespondenzadresse);Anschrift individuell (Korrespondenzadresse);Anschrift manuell geaendert (Rechnungsadresse);Anschrift individuell (Rechnungsadresse);Fristberechnung bei Debitor;Mahnfrist 1;Mahnfrist 2;Mahnfrist 3;Letzte Frist`;
|
||||
|
||||
incominginvoices.forEach(incominginvoice => {
|
||||
console.log(incominginvoice.id);
|
||||
incominginvoice.accounts.forEach(account => {
|
||||
const customersList = await server.db.select().from(customers).where(and(eq(customers.tenant, tenantId), eq(customers.active, true))).orderBy(asc(customers.customerNumber));
|
||||
const vendorsList = await server.db.select().from(vendors).where(and(eq(vendors.tenant, tenantId), eq(vendors.archived, false))).orderBy(asc(vendors.vendorNumber));
|
||||
|
||||
let file = filesIncomingInvoices.find(i => i.incominginvoice === incominginvoice.id);
|
||||
let bookinglinesStammdaten: string[] = [];
|
||||
|
||||
customersList.forEach(c => {
|
||||
const info = c.infoData as any || {};
|
||||
bookinglinesStammdaten.push(`${c.customerNumber};"${c.isCompany ? (c.name || "").substring(0,48): ''}";;"${!c.isCompany ? (c.lastname ? c.lastname : c.name) : ''}";"${!c.isCompany ? (c.firstname ? c.firstname : '') : ''}";;${c.isCompany ? 2 : 1};;;;;;;;"STR";"${info.street || ''}";;"${info.zip || ''}";"${info.city || ''}";;;"${info.special || ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`);
|
||||
});
|
||||
|
||||
let accountData = accounts.find(i => i.id === account.account)
|
||||
let buschluessel: string = "9"
|
||||
|
||||
if(account.taxType === '19'){
|
||||
buschluessel = "9"
|
||||
} else if(account.taxType === 'null') {
|
||||
buschluessel = ""
|
||||
} else if(account.taxType === '7') {
|
||||
buschluessel = "8"
|
||||
} else if(account.taxType === '19I') {
|
||||
buschluessel = "19"
|
||||
} else if(account.taxType === '7I') {
|
||||
buschluessel = "18"
|
||||
} else {
|
||||
buschluessel = "-"
|
||||
}
|
||||
|
||||
let shSelector = "S"
|
||||
let amountGross = account.amountGross ? account.amountGross : account.amountNet + account.amountTax
|
||||
|
||||
|
||||
if(Math.sign(amountGross) === 1) {
|
||||
shSelector = "S"
|
||||
} else if(Math.sign(amountGross) === -1) {
|
||||
shSelector = "H"
|
||||
}
|
||||
|
||||
let text = `ER ${incominginvoice.reference}: ${escapeString(incominginvoice.description)}`.substring(0,59)
|
||||
console.log(incominginvoice)
|
||||
bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${incominginvoice.vendor.vendorNumber};"${buschluessel}";${dayjs(incominginvoice.date).format("DDMM")};"${incominginvoice.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${incominginvoice.vendor.name}";"Kundennummer";"${incominginvoice.vendor.vendorNumber}";"Belegnummer";"${incominginvoice.reference}";"Leistungsdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
statementallocations.forEach(statementallocation => {
|
||||
|
||||
let shSelector = "S"
|
||||
|
||||
if(Math.sign(statementallocation.amount) === 1) {
|
||||
shSelector = "S"
|
||||
} else if(Math.sign(statementallocation.amount) === -1) {
|
||||
shSelector = "H"
|
||||
}
|
||||
|
||||
if(statementallocation.cd_id) {
|
||||
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"H";;;;;${statementallocation.cd_id.customer.customerNumber};${statementallocation.bs_id.account.datevNumber};"3";${dayjs(statementallocation.cd_id.documentDate).format("DDMM")};"${statementallocation.cd_id.documentNumber}";;;"${`ZE${statementallocation.description}${escapeString(statementallocation.bs_id.text)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.cd_id.customer.name}";"Kundennummer";"${statementallocation.cd_id.customer.customerNumber}";"Belegnummer";"${statementallocation.cd_id.documentNumber}";"Leistungsdatum";"${dayjs(statementallocation.cd_id.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.cd_id.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
} else if(statementallocation.ii_id) {
|
||||
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ii_id.vendor.vendorNumber};"";${dayjs(statementallocation.ii_id.date).format("DDMM")};"${statementallocation.ii_id.reference}";;;"${`ZA${statementallocation.description} ${escapeString(statementallocation.bs_id.text)} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ii_id.vendor.name}";"Kundennummer";"${statementallocation.ii_id.vendor.vendorNumber}";"Belegnummer";"${statementallocation.ii_id.reference}";"Leistungsdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
} else if(statementallocation.account) {
|
||||
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.account.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.account.number} - ${escapeString(statementallocation.account.label)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.bs_id.credName}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
} else if(statementallocation.vendor) {
|
||||
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.vendor.vendorNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.vendor.vendorNumber} - ${escapeString(statementallocation.vendor.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.vendor.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
} else if(statementallocation.customer) {
|
||||
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.customer.customerNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.customer.customerNumber} - ${escapeString(statementallocation.customer.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.customer.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
} else if(statementallocation.ownaccount) {
|
||||
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ownaccount.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.ownaccount.number} - ${escapeString(statementallocation.ownaccount.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ownaccount.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
let csvString = `${header}\n${colHeaders}\n`;
|
||||
bookingLines.forEach(line => {
|
||||
csvString += `${line}\n`;
|
||||
})
|
||||
|
||||
const buchungsstapelReader = new TextReader(csvString)
|
||||
await zipWriter.add(`EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, buchungsstapelReader)
|
||||
|
||||
/*fs.writeFile(`output/EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvString, 'utf8', function (err) {
|
||||
if (err) {
|
||||
console.log('Some error occured - file either not saved or corrupted file saved.');
|
||||
console.log(err);
|
||||
} else{
|
||||
console.log('It\'s saved!');
|
||||
}
|
||||
});*/
|
||||
|
||||
// Kreditoren/Debitoren
|
||||
let headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
|
||||
|
||||
let colHeadersStammdaten = `Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natuerl. Person);Vorname (Adressattyp natuerl. Person);Name (Adressattyp keine Angabe);Adressatentyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Strasse;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gueltig von;Adresse Gueltig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bankkonto-Nummer 1;Laenderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Haupt-Bankverb. 1;Bankverb. 1 Gueltig von;Bankverb. 1 Gueltig bis;Bankleitzahl 2;Bankbezeichnung 2;Bankkonto-Nummer 2;Laenderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Haupt-Bankverb. 2;Bankverb. 2 gueltig von;Bankverb. 2 gueltig bis;Bankleitzahl 3;Bankbezeichnung 3;Bankkonto-Nummer 3;Laenderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Haupt-Bankverb. 3;Bankverb. 3 gueltig von;Bankverb. 3 gueltig bis;Bankleitzahl 4;Bankbezeichnung 4;Bankkonto-Nummer 4;Laenderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Haupt-Bankverb. 4;Bankverb. 4 gueltig von;Bankverb. 4 gueltig bis;Bankleitzahl 5;Bankbezeichnung 5;Bankkonto-Nummer 5;Laenderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Haupt-Bankverb. 5;Bankverb. 5 gueltig von;Bankverb. 5 gueltig bis;Leerfeld;Briefanrede;Grussformel;Kunden-/Lief.-Nr.;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Waehrungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Faelligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungstraeger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Strasse (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gueltig von (Rechnungsadresse);Adresse Gueltig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bankkonto-Nummer 6;Laenderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Haupt-Bankverb. 6;Bankverb. 6 gueltig von;Bankverb. 6 gueltig bis;Bankleitzahl 7;Bankbezeichnung 7;Bankkonto-Nummer 7;Laenderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Haupt-Bankverb. 7;Bankverb. 7 gueltig von;Bankverb. 7 gueltig bis;Bankleitzahl 8;Bankbezeichnung 8;Bankkonto-Nummer 8;Laenderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Haupt-Bankverb. 8;Bankverb. 8 gueltig von;Bankverb. 8 gueltig bis;Bankleitzahl 9;Bankbezeichnung 9;Bankkonto-Nummer 9;Laenderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Haupt-Bankverb. 9;Bankverb. 9 gueltig von;Bankverb. 9 gueltig bis;Bankleitzahl 10;Bankbezeichnung 10;Bankkonto-Nummer 10;Laenderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Haupt-Bankverb. 10;Bankverb 10 Gueltig von;Bankverb 10 Gueltig bis;Nummer Fremdsystem;Insolvent;SEPA-Mandatsreferenz 1;SEPA-Mandatsreferenz 2;SEPA-Mandatsreferenz 3;SEPA-Mandatsreferenz 4;SEPA-Mandatsreferenz 5;SEPA-Mandatsreferenz 6;SEPA-Mandatsreferenz 7;SEPA-Mandatsreferenz 8;SEPA-Mandatsreferenz 9;SEPA-Mandatsreferenz 10;Verknuepftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebuehrenberechnung;Mahngebuehr 1;Mahngebuehr 2;Mahngebuehr 3;Pauschalberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3;Alternativer Suchname;Status;Anschrift manuell geaendert (Korrespondenzadresse);Anschrift individuell (Korrespondenzadresse);Anschrift manuell geaendert (Rechnungsadresse);Anschrift individuell (Rechnungsadresse);Fristberechnung bei Debitor;Mahnfrist 1;Mahnfrist 2;Mahnfrist 3;Letzte Frist`
|
||||
const {data:customers} = await server.supabase.from("customers").select().eq("tenant",tenant).order("customerNumber")
|
||||
const {data:vendors} = await server.supabase.from("vendors").select().eq("tenant",tenant).order("vendorNumber")
|
||||
|
||||
let bookinglinesStammdaten = []
|
||||
|
||||
customers.forEach(customer => {
|
||||
bookinglinesStammdaten.push(`${customer.customerNumber};"${customer.isCompany ? customer.name.substring(0,48): ''}";;"${!customer.isCompany ? (customer.lastname ? customer.lastname : customer.name) : ''}";"${!customer.isCompany ? (customer.firstname ? customer.firstname : '') : ''}";;${customer.isCompany ? 2 : 1};;;;;;;;"STR";"${customer.infoData.street ? customer.infoData.street : ''}";;"${customer.infoData.zip ? customer.infoData.zip : ''}";"${customer.infoData.city ? customer.infoData.city : ''}";;;"${customer.infoData.special ? customer.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`)
|
||||
|
||||
})
|
||||
|
||||
vendors.forEach(vendor => {
|
||||
bookinglinesStammdaten.push(`${vendor.vendorNumber};"${vendor.name.substring(0,48)}";;;;;2;;;;;;;;"STR";"${vendor.infoData.street ? vendor.infoData.street : ''}";;"${vendor.infoData.zip ? vendor.infoData.zip : ''}";"${vendor.infoData.city ? vendor.infoData.city : ''}";;;"${vendor.infoData.special ? vendor.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`)
|
||||
|
||||
})
|
||||
|
||||
let csvStringStammdaten = `${headerStammdaten}\n${colHeadersStammdaten}\n`;
|
||||
bookinglinesStammdaten.forEach(line => {
|
||||
csvStringStammdaten += `${line}\n`;
|
||||
})
|
||||
|
||||
const stammdatenReader = new TextReader(csvStringStammdaten)
|
||||
await zipWriter.add(`EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, stammdatenReader)
|
||||
|
||||
|
||||
|
||||
/*fs.writeFile(`output/EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringStammdaten, 'utf8', function (err) {
|
||||
if (err) {
|
||||
console.log('Some error occured - file either not saved or corrupted file saved.');
|
||||
console.log(err);
|
||||
} else{
|
||||
console.log('It\'s saved!');
|
||||
}
|
||||
});*/
|
||||
|
||||
//Sachkonten
|
||||
let headerSachkonten = `"EXTF";700;20;"Kontenbeschriftungen";3;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Sachkonten";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
|
||||
|
||||
let colHeadersSachkonten = `Konto;Kontenbeschriftung;Sprach-ID;Kontenbeschriftung lang`
|
||||
const {data:bankaccounts} = await server.supabase.from("bankaccounts").select().eq("tenant",tenant).order("datevNumber")
|
||||
|
||||
let bookinglinesSachkonten = []
|
||||
|
||||
bankaccounts.forEach(bankaccount => {
|
||||
bookinglinesSachkonten.push(`${bankaccount.datevNumber};"${bankaccount.name}";"de-DE";`)
|
||||
|
||||
})
|
||||
|
||||
let csvStringSachkonten = `${headerSachkonten}\n${colHeadersSachkonten}\n`;
|
||||
bookinglinesSachkonten.forEach(line => {
|
||||
csvStringSachkonten += `${line}\n`;
|
||||
})
|
||||
|
||||
const sachkontenReader = new TextReader(csvStringSachkonten)
|
||||
await zipWriter.add(`EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, sachkontenReader)
|
||||
|
||||
/*fs.writeFile(`output/EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringSachkonten, 'utf8', function (err) {
|
||||
if (err) {
|
||||
console.log('Some error occured - file either not saved or corrupted file saved.');
|
||||
console.log(err);
|
||||
} else{
|
||||
console.log('It\'s saved!');
|
||||
}
|
||||
});*/
|
||||
vendorsList.forEach(v => {
|
||||
const info = v.infoData as any || {};
|
||||
bookinglinesStammdaten.push(`${v.vendorNumber};"${(v.name || "").substring(0,48)}";;;;;2;;;;;;;;"STR";"${info.street || ''}";;"${info.zip || ''}";"${info.city || ''}";;;"${info.special || ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`);
|
||||
});
|
||||
|
||||
await zipWriter.add(
|
||||
`EXTF_Stammdaten_von_${startDateFmt}_bis_${endDateFmt}.csv`,
|
||||
new TextReader(`${headerStammdaten}\n${colHeadersStammdaten}\n` + bookinglinesStammdaten.join("\n") + "\n")
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 6. XML METADATA
|
||||
// ---------------------------------------------------------
|
||||
let obj = {
|
||||
archive: {
|
||||
'@version':"5.0",
|
||||
@@ -333,56 +395,34 @@ export async function buildExportZip(server: FastifyInstance, tenant: number, st
|
||||
date: dayjs().format("YYYY-MM-DDTHH:mm:ss")
|
||||
},
|
||||
content: {
|
||||
document: []
|
||||
document: [] as any[]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
filesCreateddocuments.forEach(file => {
|
||||
const addXmlDoc = (file: any) => {
|
||||
if(!file.path) return;
|
||||
const ext = file.path.includes('.') ? file.path.split(".").pop() : "pdf";
|
||||
obj.archive.content.document.push({
|
||||
"@guid": file.id,
|
||||
extension: {
|
||||
"@xsi:type":"File",
|
||||
"@name":`${file.id}.pdf`
|
||||
"@name":`${file.id}.${ext}`
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
filesIncomingInvoices.forEach(file => {
|
||||
obj.archive.content.document.push({
|
||||
"@guid": file.id,
|
||||
extension: {
|
||||
"@xsi:type":"File",
|
||||
"@name":`${file.id}.pdf`
|
||||
}
|
||||
})
|
||||
})
|
||||
filesCreateddocuments.forEach(addXmlDoc);
|
||||
filesIncomingInvoices.forEach(addXmlDoc);
|
||||
|
||||
let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true})
|
||||
const doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true});
|
||||
await zipWriter.add(`document.xml`, new TextReader(doc.end({pretty: true})));
|
||||
|
||||
//console.log(doc.end({pretty: true}));
|
||||
const arrayBuffer = await (await zipWriter.close()).arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
|
||||
const documentsReader = new TextReader(doc.end({pretty: true}))
|
||||
await zipWriter.add(`document.xml`, documentsReader)
|
||||
|
||||
|
||||
|
||||
|
||||
/*function toBuffer(arrayBuffer) {
|
||||
const buffer = Buffer.alloc(arrayBuffer.byteLength);
|
||||
const view = new Uint8Array(arrayBuffer);
|
||||
for (let i = 0; i < buffer.length; ++i) {
|
||||
buffer[i] = view[i];
|
||||
}
|
||||
return buffer;
|
||||
}*/
|
||||
|
||||
|
||||
const arrayBuffer = await (await zipWriter.close()).arrayBuffer()
|
||||
return Buffer.from(arrayBuffer)
|
||||
} catch(error) {
|
||||
console.log(error)
|
||||
console.error("DATEV Export Error:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -57,6 +57,7 @@ export const resourceConfig = {
|
||||
table: contracts,
|
||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
|
||||
numberRangeHolder: "contractNumber",
|
||||
mtoLoad: ["customer"],
|
||||
},
|
||||
plants: {
|
||||
table: plants,
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
FROM node:20-alpine
|
||||
# --- Stage 1: Build ---
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN mkdir -p /usr/src/nuxt-app
|
||||
WORKDIR /usr/src/nuxt-app
|
||||
COPY . .
|
||||
|
||||
RUN npm i
|
||||
# Nur Files kopieren, die für die Installation nötig sind (besseres Caching)
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Restlichen Code kopieren und bauen
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# --- Stage 2: Runtime ---
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /usr/src/nuxt-app
|
||||
|
||||
# Von der Build-Stage NUR den fertigen .output Ordner kopieren
|
||||
COPY --from=builder /usr/src/nuxt-app/.output ./.output
|
||||
|
||||
# Optional: Falls du statische Dateien aus public brauchst,
|
||||
# sind diese normalerweise bereits in .output/public enthalten.
|
||||
|
||||
ENV NUXT_HOST=0.0.0.0
|
||||
ENV NUXT_PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["node", ".output/server/index.mjs"]
|
||||
ENTRYPOINT ["node", ".output/server/index.mjs"]
|
||||
@@ -55,7 +55,7 @@ const setup = async () => {
|
||||
})
|
||||
|
||||
filetypes.value = await useEntities("filetags").select()
|
||||
documentboxes.value = await useEntities("documentboxes").select()
|
||||
//documentboxes.value = await useEntities("documentboxes").select()
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
@@ -37,7 +37,7 @@ const handleClick = async () => {
|
||||
|
||||
<UButton
|
||||
:icon="labelPrinter.connected ? 'i-heroicons-printer' : 'i-heroicons-printer'"
|
||||
:color="labelPrinter.connected ? 'green' : 'gray'"
|
||||
:color="labelPrinter.connected ? 'green' : ''"
|
||||
variant="soft"
|
||||
class="w-full justify-start"
|
||||
:loading="labelPrinter.connectLoading"
|
||||
|
||||
@@ -152,7 +152,7 @@ const links = computed(() => {
|
||||
icon: "i-heroicons-user-group",
|
||||
children: [
|
||||
... true ? [{
|
||||
label: "Anwesenheiten",
|
||||
label: "Zeiten",
|
||||
to: "/staff/time",
|
||||
icon: "i-heroicons-clock",
|
||||
}] : [],
|
||||
|
||||
@@ -2,22 +2,19 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
|
||||
// Wir erwarten eine Prop, die sagt, ob geschützt werden soll
|
||||
const props = defineProps<{
|
||||
when: boolean // z.B. true, wenn Formular dirty ist
|
||||
when: boolean
|
||||
}>()
|
||||
|
||||
const showModal = ref(false)
|
||||
const pendingNext = ref<null | ((val?: boolean) => void)>(null)
|
||||
|
||||
// --- 1. Interne Navigation (Nuxt) ---
|
||||
// --- 1. Interne Navigation ---
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
if (!props.when) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// Navigation pausieren & Modal zeigen
|
||||
pendingNext.value = next
|
||||
showModal.value = true
|
||||
})
|
||||
@@ -29,10 +26,10 @@ const confirmLeave = () => {
|
||||
|
||||
const cancelLeave = () => {
|
||||
showModal.value = false
|
||||
// Navigation wird implizit abgebrochen
|
||||
// Navigation abbrechen (pendingNext verfällt)
|
||||
}
|
||||
|
||||
// --- 2. Externe Navigation (Browser Tab schließen) ---
|
||||
// --- 2. Browser Tab schließen ---
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (props.when) {
|
||||
e.preventDefault()
|
||||
@@ -50,12 +47,12 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
|
||||
<div class="guard-modal">
|
||||
|
||||
<div class="guard-header">
|
||||
<slot name="title">Seite wirklich verlassen?</slot>
|
||||
<slot name="title">Seite verlassen?</slot>
|
||||
</div>
|
||||
|
||||
<div class="guard-body">
|
||||
<slot>
|
||||
Du hast ungespeicherte Änderungen. Diese gehen verloren, wenn du die Seite verlässt.
|
||||
Du hast ungespeicherte Änderungen. Wenn du die Seite verlässt, gehen diese verloren.
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +61,7 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
|
||||
Nein, bleiben
|
||||
</button>
|
||||
<button @click="confirmLeave" class="btn-confirm">
|
||||
Ja, verlassen
|
||||
Ja, verwerfen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -74,25 +71,65 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Basis-Styling - passe dies an dein Design System an */
|
||||
/* --- Layout & Animation --- */
|
||||
.guard-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 9999; backdrop-filter: blur(2px);
|
||||
z-index: 9999; backdrop-filter: blur(4px);
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.guard-modal {
|
||||
background: white; padding: 24px; border-radius: 12px;
|
||||
width: 90%; max-width: 400px; box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.guard-header { font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem; }
|
||||
.guard-body { margin-bottom: 1.5rem; color: #4a5568; }
|
||||
.guard-actions { display: flex; justify-content: flex-end; gap: 12px; }
|
||||
|
||||
/* Buttons */
|
||||
button { padding: 8px 16px; border-radius: 6px; cursor: pointer; border: none; font-weight: 600;}
|
||||
.btn-cancel { background: #edf2f7; color: #2d3748; }
|
||||
.btn-cancel:hover { background: #e2e8f0; }
|
||||
.btn-confirm { background: #e53e3e; color: white; }
|
||||
.btn-confirm:hover { background: #c53030; }
|
||||
.guard-modal {
|
||||
width: 90%; max-width: 420px;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.guard-header {
|
||||
font-size: 1.125rem; font-weight: 600; margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.guard-body {
|
||||
margin-bottom: 1.5rem; font-size: 0.95rem; line-height: 1.5;
|
||||
}
|
||||
|
||||
.guard-actions {
|
||||
display: flex; justify-content: flex-end; gap: 12px;
|
||||
}
|
||||
|
||||
/* --- Buttons Basis --- */
|
||||
button {
|
||||
padding: 8px 16px; border-radius: 6px; cursor: pointer; border: none; font-weight: 500; font-size: 0.9rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------- */
|
||||
/* LIGHT MODE (Default) */
|
||||
/* --------------------------------------------------------- */
|
||||
.guard-modal {
|
||||
background: white;
|
||||
color: #1f2937; /* gray-800 */
|
||||
}
|
||||
|
||||
.guard-body {
|
||||
color: #4b5563; /* gray-600 */
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f3f4f6; /* gray-100 */
|
||||
color: #374151; /* gray-700 */
|
||||
}
|
||||
.btn-cancel:hover { background: #e5e7eb; }
|
||||
|
||||
.btn-confirm {
|
||||
background: #ef4444; /* red-500 */
|
||||
color: white;
|
||||
}
|
||||
.btn-confirm:hover { background: #dc2626; }
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
@@ -159,7 +159,16 @@ const submit = async () => {
|
||||
@input="e => form.startDate = new Date(e.target.value)"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Ende">
|
||||
<UFormGroup label="Dauer (Stunden)">
|
||||
<input
|
||||
type="number"
|
||||
step="0.25"
|
||||
placeholder="z.B. 1.5"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 sm:text-sm sm:leading-6 px-2"
|
||||
@input="e => form.endDate = dayjs(form.startDate).add(parseFloat(e.target.value), 'hour').toDate()"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Ende" class="col-span-2">
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 sm:text-sm sm:leading-6 px-2"
|
||||
|
||||
@@ -103,7 +103,8 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
start: startIso, // Die eingegebene Startzeit
|
||||
end: endIso, // Die eingegebene Endzeit (oder null)
|
||||
type: state.type,
|
||||
description: state.description
|
||||
description: state.description,
|
||||
user_id: props.defaultUserId
|
||||
})
|
||||
|
||||
toast.add({ title: 'Zeit manuell erfasst', color: 'green' })
|
||||
|
||||
@@ -42,9 +42,7 @@ const setupPage = async () => {
|
||||
})
|
||||
draftInvoicesCount.value = draftDocuments.length
|
||||
|
||||
countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet").length
|
||||
|
||||
|
||||
countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet" && !i.archived).length
|
||||
}
|
||||
|
||||
setupPage()
|
||||
@@ -78,10 +76,10 @@ setupPage()
|
||||
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk / 0,00€</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="break-all">ToDo Eingangsrechnungsrechnungen:</td>
|
||||
<td class="break-all">Vorbereitete Eingangsrechnungen:</td>
|
||||
<td
|
||||
v-if="countPreparedOpenIncomingInvoices > 0"
|
||||
class="text-orange-500 font-bold text-nowrap"
|
||||
class="text-orange-500 font-bold text-wrap"
|
||||
>{{countPreparedOpenIncomingInvoices}} Stk </td>
|
||||
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk</td>
|
||||
</tr>
|
||||
|
||||
@@ -91,15 +91,18 @@ export const useStaffTime = () => {
|
||||
}
|
||||
|
||||
// 🆕 NEU: Manuellen Eintrag erstellen (Vergangenheit oder Zeitraum)
|
||||
const createEntry = async (data: { start: string, end: string | null, type: string, description: string }) => {
|
||||
const createEntry = async (data: { start: string, end: string | null, type: string, description: string, user_id: string }) => {
|
||||
// 1. Start Event senden
|
||||
// Wir nutzen den dynamischen Typ (work_start, vacation_start etc.)
|
||||
console.log(data)
|
||||
|
||||
await $api('/api/staff/time/event', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
eventtype: `${data.type}_start`,
|
||||
eventtime: data.start,
|
||||
payload: { description: data.description }
|
||||
payload: { description: data.description },
|
||||
user_id: data.user_id,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -109,7 +112,9 @@ export const useStaffTime = () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
eventtype: `${data.type}_end`,
|
||||
eventtime: data.end
|
||||
eventtime: data.end,
|
||||
payload: { description: data.description },
|
||||
user_id: data.user_id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -236,11 +236,17 @@ const footerLinks = [
|
||||
<template #footer>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
|
||||
|
||||
<UColorModeButton />
|
||||
<LabelPrinterButton/>
|
||||
|
||||
|
||||
|
||||
<!-- Footer Links -->
|
||||
<UDashboardSidebarLinks :links="footerLinks" />
|
||||
|
||||
|
||||
|
||||
<UDivider class="sticky bottom-0" />
|
||||
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
|
||||
</div>
|
||||
|
||||
@@ -204,7 +204,8 @@ const setupPage = async () => {
|
||||
processDieselPosition()
|
||||
}
|
||||
|
||||
} else if (route.query.loadMode === "finalInvoice") {
|
||||
}
|
||||
else if (route.query.loadMode === "finalInvoice") {
|
||||
let linkedDocuments = (await useEntities("createddocuments").select()).filter(i => JSON.parse(route.query.linkedDocuments).includes(i.id))
|
||||
|
||||
//TODO: Implement Checking for Same Customer, Contact and Project
|
||||
@@ -355,6 +356,8 @@ const setupPage = async () => {
|
||||
|
||||
}
|
||||
|
||||
await setCustomerData(null, true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -459,7 +462,11 @@ const setCustomerData = async (customerId, loadOnlyAdress = false) => {
|
||||
|
||||
let customer = customers.value.find(i => i.id === itemInfo.value.customer)
|
||||
console.log(customer)
|
||||
itemInfo.value.contact = null
|
||||
if(!loadOnlyAdress) {
|
||||
itemInfo.value.contact = null
|
||||
}
|
||||
|
||||
|
||||
if (customer) {
|
||||
itemInfo.value.address.street = customer.infoData.street
|
||||
itemInfo.value.address.zip = customer.infoData.zip
|
||||
@@ -1824,7 +1831,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
>
|
||||
<InputGroup>
|
||||
<USelectMenu
|
||||
:options="contacts.filter(i => i.customer === itemInfo.customer)"
|
||||
:options="contacts.filter(i => i.customer?.id === itemInfo.customer)"
|
||||
option-attribute="fullName"
|
||||
value-attribute="id"
|
||||
:search-attributes="['name']"
|
||||
@@ -2057,7 +2064,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
>
|
||||
<InputGroup>
|
||||
<USelectMenu
|
||||
:options="plants.filter(i => i.customer === itemInfo.customer)"
|
||||
:options="plants.filter(i => i.customer?.id === itemInfo.customer)"
|
||||
v-model="itemInfo.plant"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
@@ -2095,7 +2102,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
>
|
||||
<InputGroup>
|
||||
<USelectMenu
|
||||
:options="projects.filter(i => i.customer === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
|
||||
:options="projects.filter(i => i.customer?.id === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
|
||||
v-model="itemInfo.project"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
@@ -2134,7 +2141,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
>
|
||||
<InputGroup>
|
||||
<USelectMenu
|
||||
:options="contracts.filter(i => i.customer === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
|
||||
:options="contracts.filter(i => i.customer?.id === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
|
||||
v-model="itemInfo.contract"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
|
||||
@@ -81,7 +81,7 @@ const openBankstatements = () => {
|
||||
E-Mail
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?linkedDocument=${itemInfo.id}&loadMode=storno`)"
|
||||
@click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
v-if="itemInfo.type === 'invoices' || itemInfo.type === 'advanceInvoices'"
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs"
|
||||
|
||||
// --- Helper für Schnellauswahl ---
|
||||
|
||||
// Setzt ein spezifisches Quartal (Q1-Q4), optional mit Jahresangabe
|
||||
const setQuarter = (quarter: number, year: number = dayjs().year()) => {
|
||||
const startMonth = (quarter - 1) * 3 // Q1=0 (Jan), Q2=3 (Apr), etc.
|
||||
|
||||
// .toDate() ist wichtig für den DatePicker
|
||||
createExportData.value.start_date = dayjs().year(year).month(startMonth).startOf('month').toDate()
|
||||
createExportData.value.end_date = dayjs().year(year).month(startMonth + 2).endOf('month').toDate()
|
||||
}
|
||||
|
||||
// Berechnet automatisch das vorherige Quartal (inkl. Jahreswechsel, falls wir in Q1 sind)
|
||||
const setLastQuarter = () => {
|
||||
const currentMonth = dayjs().month() // 0 bis 11
|
||||
const currentQuarter = Math.floor(currentMonth / 3) + 1 // 1 bis 4
|
||||
|
||||
if (currentQuarter === 1) {
|
||||
// Wenn wir in Q1 sind, ist das letzte Quartal Q4 des Vorjahres
|
||||
setQuarter(4, dayjs().year() - 1)
|
||||
} else {
|
||||
// Sonst einfach das vorherige Quartal im aktuellen Jahr
|
||||
setQuarter(currentQuarter - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const setMonthPreset = (type: 'current' | 'last') => {
|
||||
let date = dayjs()
|
||||
if (type === 'last') {
|
||||
date = date.subtract(1, 'month')
|
||||
}
|
||||
|
||||
createExportData.value.start_date = date.startOf('month').toDate()
|
||||
createExportData.value.end_date = date.endOf('month').toDate()
|
||||
}
|
||||
// ---------------------------------
|
||||
|
||||
const exports = ref([])
|
||||
|
||||
const auth = useAuthStore()
|
||||
@@ -46,8 +83,6 @@ const createExport = async () => {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,28 +98,16 @@ const createExport = async () => {
|
||||
>+ SEPA</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UTable
|
||||
:rows="exports"
|
||||
:columns="[
|
||||
{
|
||||
key: 'created_at',
|
||||
label: 'Erstellt am',
|
||||
},{
|
||||
key: 'start_date',
|
||||
label: 'Start',
|
||||
},{
|
||||
key: 'end_date',
|
||||
label: 'Ende',
|
||||
},{
|
||||
key: 'valid_until',
|
||||
label: 'Gültig bis',
|
||||
},{
|
||||
key: 'type',
|
||||
label: 'Typ',
|
||||
},{
|
||||
key: 'download',
|
||||
label: 'Download',
|
||||
},
|
||||
{ key: 'created_at', label: 'Erstellt am' },
|
||||
{ key: 'start_date', label: 'Start' },
|
||||
{ key: 'end_date', label: 'Ende' },
|
||||
{ key: 'valid_until', label: 'Gültig bis' },
|
||||
{ key: 'type', label: 'Typ' },
|
||||
{ key: 'download', label: 'Download' },
|
||||
]"
|
||||
>
|
||||
<template #created_at-data="{row}">
|
||||
@@ -100,11 +123,7 @@ const createExport = async () => {
|
||||
{{dayjs(row.valid_until).format("DD.MM.YYYY HH:mm")}}
|
||||
</template>
|
||||
<template #download-data="{row}">
|
||||
<UButton
|
||||
@click="downloadFile(row)"
|
||||
>
|
||||
Download
|
||||
</UButton>
|
||||
<UButton @click="downloadFile(row)">Download</UButton>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
@@ -114,46 +133,78 @@ const createExport = async () => {
|
||||
Export erstellen
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
label="Start:"
|
||||
>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="createExportData.start_date ? dayjs(createExportData.start_date).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
class="mx-auto"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="createExportData.start_date" @close="close" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-xs text-gray-400 w-16">Monat:</span>
|
||||
<UButton size="2xs" color="white" variant="solid" @click="setMonthPreset('last')">Letzter</UButton>
|
||||
<UButton size="2xs" color="white" variant="solid" @click="setMonthPreset('current')">Aktuell</UButton>
|
||||
</div>
|
||||
|
||||
<UFormGroup
|
||||
label="Ende:"
|
||||
>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="createExportData.end_date ? dayjs(createExportData.end_date).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
class="mx-auto"
|
||||
/>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-xs text-gray-400 w-16">Quartal:</span>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="createExportData.end_date" @close="close" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
<UButton
|
||||
size="2xs"
|
||||
color="white"
|
||||
variant="solid"
|
||||
@click="setLastQuarter()"
|
||||
class="mr-2 border-r border-gray-200 dark:border-gray-600 pr-3"
|
||||
>
|
||||
Letztes
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-for="q in 4"
|
||||
:key="q"
|
||||
size="2xs"
|
||||
color="white"
|
||||
variant="solid"
|
||||
@click="setQuarter(q)"
|
||||
>
|
||||
Q{{ q }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<UFormGroup label="Start:" class="flex-1">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="createExportData.start_date ? dayjs(createExportData.start_date).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
/>
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="createExportData.start_date" @close="close" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Ende:" class="flex-1">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="createExportData.end_date ? dayjs(createExportData.end_date).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
/>
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="createExportData.end_date" @close="close" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="createExport"
|
||||
>
|
||||
Erstellen
|
||||
</UButton>
|
||||
<div class="flex justify-end">
|
||||
<UButton @click="createExport">
|
||||
Erstellen
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</UCard>
|
||||
@@ -162,5 +213,4 @@ const createExport = async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -122,7 +122,7 @@ const totalCalculated = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
totalGross = Number(totalNet + totalAmount19Tax)
|
||||
totalGross = Number(totalNet + totalAmount19Tax + totalAmount7Tax)
|
||||
|
||||
return {
|
||||
totalNet,
|
||||
@@ -147,7 +147,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
|
||||
} else {
|
||||
item.state = "Entwurf"
|
||||
}
|
||||
const data = await useEntities('incominginvoices').update(itemInfo.value.id,item)
|
||||
const data = await useEntities('incominginvoices').update(itemInfo.value.id,item,true)
|
||||
}
|
||||
|
||||
const findIncomingInvoiceErrors = computed(() => {
|
||||
|
||||
@@ -1,220 +1,352 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const dataStore = useDataStore()
|
||||
const toast = useToast()
|
||||
|
||||
defineShortcuts({
|
||||
'+': () => {
|
||||
editTemplateModalOpen.value = true
|
||||
}
|
||||
})
|
||||
// useEntities Initialisierung für 'texttemplates'
|
||||
const { select, create, update } = useEntities("texttemplates")
|
||||
|
||||
// --- State ---
|
||||
const editTemplateModalOpen = ref(false)
|
||||
const itemInfo = ref({})
|
||||
const texttemplates = ref([])
|
||||
const loading = ref(true)
|
||||
const isSaving = ref(false)
|
||||
const textareaRef = ref(null)
|
||||
|
||||
const setup = async () => {
|
||||
texttemplates.value = (await useEntities("texttemplates").select()).filter(i => !i.archived)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
// Tabelle Expand State
|
||||
const expand = ref({
|
||||
openedRows: [],
|
||||
row: {}
|
||||
})
|
||||
|
||||
// --- Variablen Definitionen ---
|
||||
const variableDefinitions = [
|
||||
{ key: '{{vorname}}', label: 'Vorname', desc: 'Vorname des Kunden' },
|
||||
{ key: '{{nachname}}', label: 'Nachname', desc: 'Nachname des Kunden' },
|
||||
{ key: '{{anrede}}', label: 'Anrede', desc: 'Formelle Anrede' },
|
||||
{ key: '{{titel}}', label: 'Titel', desc: 'Titel des Kunden' },
|
||||
{ key: '{{zahlungsziel_in_tagen}}', label: 'Zahlungsziel', desc: 'In Tagen' },
|
||||
{ key: '{{lohnkosten}}', label: 'Lohnkosten', desc: 'Ausgewiesene Lohnkosten' },
|
||||
]
|
||||
|
||||
// --- Shortcuts ---
|
||||
defineShortcuts({
|
||||
'+': () => openModal()
|
||||
})
|
||||
|
||||
// --- Data Fetching ---
|
||||
const refreshData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// select() filtert bereits archivierte Einträge, wenn dataType.isArchivable true ist
|
||||
texttemplates.value = await select()
|
||||
} catch (e) {
|
||||
toast.add({ title: 'Fehler beim Laden', description: e.message, color: 'rose' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initialer Load
|
||||
onMounted(() => {
|
||||
refreshData()
|
||||
})
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
const openModal = (item = null) => {
|
||||
if (item) {
|
||||
// Deep Copy um Reaktivitätsprobleme beim Abbrechen zu vermeiden
|
||||
itemInfo.value = JSON.parse(JSON.stringify(item))
|
||||
} else {
|
||||
// Reset für Erstellen
|
||||
itemInfo.value = {
|
||||
name: '',
|
||||
documentType: 'offer', // Default
|
||||
pos: 'startText',
|
||||
text: '',
|
||||
default: false
|
||||
}
|
||||
}
|
||||
editTemplateModalOpen.value = true
|
||||
}
|
||||
|
||||
const insertVariable = (variableKey) => {
|
||||
itemInfo.value.text = (itemInfo.value.text || '') + variableKey + ' '
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
isSaving.value = true
|
||||
try {
|
||||
// create(payload, noRedirect) -> Wir setzen noRedirect auf true
|
||||
await create(itemInfo.value, true)
|
||||
|
||||
// Hinweis: Erfolgs-Toast kommt bereits aus useEntities
|
||||
editTemplateModalOpen.value = false
|
||||
await refreshData()
|
||||
} catch (e) {
|
||||
toast.add({ title: 'Fehler', description: 'Konnte nicht erstellt werden.', color: 'rose' })
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
isSaving.value = true
|
||||
try {
|
||||
// update(id, payload, noRedirect) -> Wir setzen noRedirect auf true
|
||||
await update(itemInfo.value.id, itemInfo.value, true)
|
||||
|
||||
// Hinweis: Erfolgs-Toast kommt bereits aus useEntities
|
||||
editTemplateModalOpen.value = false
|
||||
await refreshData()
|
||||
} catch (e) {
|
||||
toast.add({title: 'Fehler', description: 'Konnte nicht gespeichert werden.', color: 'rose'})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleArchive = async (row) => {
|
||||
try {
|
||||
// Wir nutzen update mit archived: true und noRedirect, um auf der Seite zu bleiben
|
||||
await update(row.id, {archived: true}, true)
|
||||
|
||||
await refreshData()
|
||||
} catch (e) {
|
||||
toast.add({title: 'Fehler', description: 'Konnte nicht archiviert werden.', color: 'rose'})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper für Labels (falls dataStore noch lädt oder Key fehlt)
|
||||
const getDocLabel = (type) => {
|
||||
return dataStore.documentTypesForCreation?.[type]?.label || type
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardNavbar
|
||||
title="Text Vorlagen"
|
||||
>
|
||||
<UDashboardNavbar title="Text Vorlagen">
|
||||
<template #right>
|
||||
<UButton
|
||||
@click="editTemplateModalOpen = true, itemInfo = {}"
|
||||
icon="i-heroicons-plus"
|
||||
@click="openModal()"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
>
|
||||
+ Erstellen
|
||||
Erstellen
|
||||
</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
<UDashboardPanelContent>
|
||||
<UCard class="mx-5">
|
||||
<template #header>
|
||||
Variablen
|
||||
</template>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="text-left">Variable</th>
|
||||
<th class="text-left">Beschreibung</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vorname</td>
|
||||
<td>Vorname</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>nachname</td>
|
||||
<td>Nachname</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>zahlungsziel_in_tagen</td>
|
||||
<td>Zahlungsziel in Tagen</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>lohnkosten</td>
|
||||
<td>Lohnkosten Verkauf</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>titel</td>
|
||||
<td>Titel</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>anrede</td>
|
||||
<td>Anrede</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</UCard>
|
||||
<UDashboardPanelContent>
|
||||
|
||||
<UAlert
|
||||
icon="i-heroicons-information-circle"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
title="Platzhalter nutzen"
|
||||
description="Nutzen Sie die Variablen im Editor, um dynamische Inhalte (wie Kundennamen) automatisch einzufügen."
|
||||
class="mb-4 mx-5 mt-2"
|
||||
/>
|
||||
|
||||
<UTable
|
||||
class="mt-3"
|
||||
:rows="texttemplates"
|
||||
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||
:loading="loading"
|
||||
v-model:expand="expand" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Textvorlagen anzuzeigen' }"
|
||||
:columns="[{key:'name',label:'Name'},{key:'documentType',label:'Dokumententyp'},{key:'default',label:'Standard'},{key:'pos',label:'Position'}]"
|
||||
v-model:expand="expand"
|
||||
:empty-state="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }"
|
||||
:columns="[
|
||||
{ key: 'name', label: 'Bezeichnung' },
|
||||
{ key: 'documentType', label: 'Verwendung' },
|
||||
{ key: 'pos', label: 'Position' },
|
||||
{ key: 'default', label: 'Standard' },
|
||||
{ key: 'actions', label: '' }
|
||||
]"
|
||||
>
|
||||
<template #documentType-data="{row}">
|
||||
{{dataStore.documentTypesForCreation[row.documentType].label}}
|
||||
<template #name-data="{ row }">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span>
|
||||
</template>
|
||||
<template #default-data="{row}">
|
||||
{{row.default ? "Ja" : "Nein"}}
|
||||
|
||||
<template #documentType-data="{ row }">
|
||||
<UBadge color="gray" variant="soft">
|
||||
{{ getDocLabel(row.documentType) }}
|
||||
</UBadge>
|
||||
</template>
|
||||
<template #pos-data="{row}">
|
||||
<span v-if="row.pos === 'startText'">Einleitung</span>
|
||||
<span v-else-if="row.pos === 'endText'">Endtext</span>
|
||||
|
||||
<template #pos-data="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
:name="row.pos === 'startText' ? 'i-heroicons-bars-arrow-down' : 'i-heroicons-bars-arrow-up'"
|
||||
class="w-4 h-4 text-gray-500"
|
||||
/>
|
||||
<span>{{ row.pos === 'startText' ? 'Einleitung' : 'Endtext' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default-data="{ row }">
|
||||
<UIcon v-if="row.default" name="i-heroicons-check-circle-20-solid" class="text-green-500"/>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
|
||||
<template #actions-data="{ row }">
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-pencil-square" @click="openModal(row)"/>
|
||||
</template>
|
||||
|
||||
<template #expand="{ row }">
|
||||
<div class="p-4">
|
||||
<p class="text-2xl">{{dataStore.documentTypesForCreation[row.documentType].label}}</p>
|
||||
<p class="text-xl mt-3">{{row.pos === 'startText' ? 'Einleitung' : 'Ende'}}</p>
|
||||
<p class="text-justify mt-3">{{row.text}}</p>
|
||||
<UButton
|
||||
class="mt-3 mr-3"
|
||||
@click="itemInfo = row;
|
||||
editTemplateModalOpen = true"
|
||||
variant="outline"
|
||||
>Bearbeiten</UButton>
|
||||
<ButtonWithConfirm
|
||||
color="rose"
|
||||
variant="outline"
|
||||
@confirmed="dataStore.updateItem('texttemplates',{id: row.id,archived: true}),
|
||||
setup"
|
||||
>
|
||||
<template #button>
|
||||
Archivieren
|
||||
</template>
|
||||
<template #header>
|
||||
<span class="text-md dark:text-whitetext-black font-bold">Archivieren bestätigen</span>
|
||||
</template>
|
||||
Möchten Sie diesen Ausgangsbeleg wirklich archivieren?
|
||||
</ButtonWithConfirm>
|
||||
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 rounded-b-lg border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-bold uppercase text-gray-500 mb-1">Vorschau</h4>
|
||||
<p class="text-gray-800 dark:text-gray-200 whitespace-pre-line p-3 bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-700 text-sm">
|
||||
{{ row.text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<ButtonWithConfirm
|
||||
color="rose"
|
||||
variant="soft"
|
||||
icon="i-heroicons-archive-box"
|
||||
@confirmed="handleArchive(row)"
|
||||
>
|
||||
<template #button>Archivieren</template>
|
||||
<template #header>
|
||||
<span class="font-bold">Wirklich archivieren?</span>
|
||||
</template>
|
||||
Die Vorlage "{{ row.name }}" wird archiviert.
|
||||
</ButtonWithConfirm>
|
||||
|
||||
<UButton
|
||||
icon="i-heroicons-pencil"
|
||||
@click="openModal(row)"
|
||||
>
|
||||
Bearbeiten
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
|
||||
<!-- <div class="w-3/4 mx-auto mt-5">
|
||||
<UCard
|
||||
v-for="template in dataStore.texttemplates"
|
||||
class="mb-3"
|
||||
>
|
||||
<p class="text-2xl">{{dataStore.documentTypesForCreation[template.documentType].label}}</p>
|
||||
<p class="text-xl">{{template.pos === 'startText' ? 'Einleitung' : 'Ende'}}</p>
|
||||
<p class="text-justify">{{template.text}}</p>
|
||||
<UButton
|
||||
@click="itemInfo = template;
|
||||
editTemplateModalOpen = true"
|
||||
icon="i-heroicons-pencil-solid"
|
||||
variant="outline"
|
||||
/>
|
||||
</UCard>
|
||||
</div>-->
|
||||
</UDashboardPanelContent>
|
||||
|
||||
<UModal
|
||||
v-model="editTemplateModalOpen"
|
||||
>
|
||||
<UCard class="h-full">
|
||||
<UModal v-model="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }">
|
||||
<UCard>
|
||||
<template #header>
|
||||
{{itemInfo.id ? 'Vorlage bearbeiten' : 'Vorlage erstellen'}}
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ itemInfo.id ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen' }}
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark" @click="editTemplateModalOpen = false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm class="h-full">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<UFormGroup
|
||||
label="Name:"
|
||||
>
|
||||
<UInput
|
||||
v-model="itemInfo.name"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="Dokumententyp:"
|
||||
>
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<UFormGroup label="Bezeichnung" required>
|
||||
<UInput v-model="itemInfo.name" placeholder="z.B. Standard Angebotstext" icon="i-heroicons-tag"/>
|
||||
</UFormGroup>
|
||||
|
||||
<USelectMenu
|
||||
:options="Object.keys(dataStore.documentTypesForCreation).filter(i => i !== 'serialInvoices').map(i => {
|
||||
return {
|
||||
label: dataStore.documentTypesForCreation[i].label,
|
||||
key: i
|
||||
}
|
||||
})"
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
v-model="itemInfo.documentType"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="Position:"
|
||||
>
|
||||
<USelectMenu
|
||||
:options="[{label:'Einleitung',key: 'startText'},{label:'Ende',key: 'endText'}]"
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
v-model="itemInfo.pos"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="Text:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="itemInfo.text"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UForm>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormGroup label="Dokumententyp" required>
|
||||
<USelectMenu
|
||||
v-model="itemInfo.documentType"
|
||||
:options="Object.keys(dataStore.documentTypesForCreation || {})
|
||||
.filter(i => i !== 'serialInvoices')
|
||||
.map(i => ({ label: dataStore.documentTypesForCreation[i].label, key: i }))"
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Position" required>
|
||||
<USelectMenu
|
||||
v-model="itemInfo.pos"
|
||||
:options="[
|
||||
{ label: 'Einleitung (Oben)', key: 'startText' },
|
||||
{ label: 'Endtext (Unten)', key: 'endText' }
|
||||
]"
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</div>
|
||||
|
||||
<UFormGroup label="Text Inhalt" required help="Klicken Sie rechts auf eine Variable, um sie einzufügen.">
|
||||
<UTextarea
|
||||
ref="textareaRef"
|
||||
v-model="itemInfo.text"
|
||||
:rows="10"
|
||||
placeholder="Sehr geehrte Damen und Herren..."
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UCheckbox v-model="itemInfo.default" label="Als Standard für diesen Typ verwenden"/>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 h-fit">
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-variable"/>
|
||||
Variablen
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 mb-4">
|
||||
Diese Platzhalter werden beim Erstellen des Dokuments ersetzt.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
v-for="v in variableDefinitions"
|
||||
:key="v.key"
|
||||
@click="insertVariable(v.key)"
|
||||
class="group flex items-center justify-between p-2 rounded hover:bg-white dark:hover:bg-gray-700 border border-transparent hover:border-gray-200 dark:hover:border-gray-600 transition-colors text-left"
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<code
|
||||
class="text-xs font-bold text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-950/50 px-1 py-0.5 rounded">{{
|
||||
v.key
|
||||
}}</code>
|
||||
<div class="text-xs text-gray-500 mt-0.5">{{ v.desc }}</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-plus-circle" class="w-5 h-5 text-gray-300 group-hover:text-primary-500"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TODO: Update und Create -->
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="dataStore.createNewItem('texttemplates',itemInfo);
|
||||
editTemplateModalOpen = false"
|
||||
v-if="!itemInfo.id"
|
||||
>Erstellen</UButton>
|
||||
<UButton
|
||||
@click="dataStore.updateItem('texttemplates',itemInfo);
|
||||
editTemplateModalOpen = false"
|
||||
v-if="itemInfo.id"
|
||||
>Speichern</UButton>
|
||||
<div class="flex justify-end gap-3">
|
||||
<UButton color="gray" variant="ghost" @click="editTemplateModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="!itemInfo.id"
|
||||
color="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleCreate"
|
||||
icon="i-heroicons-plus"
|
||||
>
|
||||
Erstellen
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-else
|
||||
color="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleUpdate"
|
||||
icon="i-heroicons-check"
|
||||
>
|
||||
Speichern
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</UModal>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -119,8 +119,8 @@ async function loadWorkingTimeInfo() {
|
||||
|
||||
// Erstellt Query-Parameter für den neuen Backend-Endpunkt
|
||||
const queryParams = new URLSearchParams({
|
||||
from: selectedStartDay.value,
|
||||
to: selectedEndDay.value,
|
||||
from: $dayjs(selectedStartDay.value).format("YYYY-MM-DD"),
|
||||
to: $dayjs(selectedEndDay.value).format("YYYY-MM-DD"),
|
||||
targetUserId: evaluatedUserId.value,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,31 +1,86 @@
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig()
|
||||
const toast = useToast()
|
||||
|
||||
const api = $fetch.create({
|
||||
baseURL: config.public.apiBase,
|
||||
credentials: "include",
|
||||
async onRequest({options}) {
|
||||
// Token aus Cookie holen
|
||||
let token: string | null | undefined = ""
|
||||
|
||||
token = useCookie("token").value
|
||||
async onRequest({ options }) {
|
||||
const token = useCookie("token").value
|
||||
|
||||
|
||||
|
||||
// Falls im Request explizit ein anderer JWT übergeben wird → diesen verwenden
|
||||
if (options.context && (options.context as any).jwt) {
|
||||
token = (options.context as any).jwt
|
||||
}
|
||||
|
||||
if (token) {
|
||||
// Falls im Request explizit ein anderer JWT übergeben wird
|
||||
if (options.context?.jwt) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${options.context.jwt}`,
|
||||
}
|
||||
} else if (token) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async onRequestError({error}) {
|
||||
toast.add({
|
||||
title: "Fehler",
|
||||
description: "Eine Anfrage konnte nicht ausgeführt werden.",
|
||||
color: 'red',
|
||||
icon: 'i-heroicons-exclamation-triangle-20-solid',
|
||||
timeout: 5000 // Bleibt 5 Sekunden sichtbar
|
||||
})
|
||||
},
|
||||
|
||||
async onResponseError({ response }) {
|
||||
// Toasts nur im Client anzeigen
|
||||
console.log(response)
|
||||
if (!process.client) return
|
||||
|
||||
const status = response.status
|
||||
let title = "Fehler"
|
||||
let description = "Ein unerwarteter Fehler ist aufgetreten."
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
title = "Anfrage fehlerhaft"
|
||||
description = "Die Daten konnten nicht korrekt verarbeitet werden."
|
||||
break
|
||||
case 401:
|
||||
title = "Nicht angemeldet"
|
||||
description = "Deine Sitzung ist abgelaufen oder ungültig."
|
||||
// Optional: useCookie('token').value = null
|
||||
break
|
||||
case 403:
|
||||
title = "Zugriff verweigert"
|
||||
description = "Du hast keine Berechtigung für diesen Bereich."
|
||||
break
|
||||
case 404:
|
||||
title = "Nicht gefunden"
|
||||
description = "Die gesuchte Ressource wurde nicht gefunden."
|
||||
break
|
||||
case 500:
|
||||
title = "Server-Fehler"
|
||||
description = "Internes Problem. Bitte versuche es später erneut."
|
||||
break
|
||||
}
|
||||
|
||||
// Nuxt UI Toast Notification
|
||||
toast.add({
|
||||
title: title,
|
||||
description: description,
|
||||
color: 'red',
|
||||
icon: 'i-heroicons-exclamation-triangle-20-solid',
|
||||
timeout: 5000 // Bleibt 5 Sekunden sichtbar
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return { provide: { api } }
|
||||
return {
|
||||
provide: {
|
||||
api
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2499,8 +2499,8 @@ export const useDataStore = defineStore('data', () => {
|
||||
component: vehicle,
|
||||
inputType: "select",
|
||||
selectDataType: "vehicles",
|
||||
selectOptionAttribute: "licensePlate",
|
||||
selectSearchAttributes: ['licensePlate'],
|
||||
selectOptionAttribute: "license_plate",
|
||||
selectSearchAttributes: ['license_plate','vin'],
|
||||
},
|
||||
{
|
||||
key: "inventoryitem",
|
||||
|
||||
Reference in New Issue
Block a user