Added Backend
This commit is contained in:
38
backend/src/utils/crypt.ts
Normal file
38
backend/src/utils/crypt.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import crypto from "crypto";
|
||||
import {secrets} from "./secrets"
|
||||
const ALGORITHM = "aes-256-gcm";
|
||||
|
||||
|
||||
|
||||
|
||||
export function encrypt(text) {
|
||||
const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex");
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return {
|
||||
iv: iv.toString("hex"),
|
||||
content: encrypted.toString("hex"),
|
||||
tag: tag.toString("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
export function decrypt({ iv, content, tag }) {
|
||||
const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex");
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
ENCRYPTION_KEY,
|
||||
Buffer.from(iv, "hex")
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(Buffer.from(content, "hex")),
|
||||
decipher.final(),
|
||||
]);
|
||||
|
||||
return decrypted.toString("utf8");
|
||||
}
|
||||
27
backend/src/utils/dbSearch.ts
Normal file
27
backend/src/utils/dbSearch.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ilike, or } from "drizzle-orm"
|
||||
|
||||
/**
|
||||
* Erzeugt eine OR-Suchbedingung über mehrere Spalten
|
||||
*
|
||||
* @param table - Drizzle Table Schema
|
||||
* @param columns - Array der Spaltennamen (property names im schema)
|
||||
* @param search - Suchbegriff
|
||||
*/
|
||||
export function buildSearchWhere(table: any, columns: string[], search: string) {
|
||||
if (!search || !columns.length) return undefined
|
||||
|
||||
const term = `%${search.toLowerCase()}%`
|
||||
|
||||
const parts = columns
|
||||
.map((colName) => {
|
||||
const col = table[colName]
|
||||
if (!col) return null
|
||||
return ilike(col, term)
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (parts.length === 0) return undefined
|
||||
|
||||
// @ts-ignore
|
||||
return or(...parts)
|
||||
}
|
||||
103
backend/src/utils/diff.ts
Normal file
103
backend/src/utils/diff.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import {diffTranslations} from "./diffTranslations";
|
||||
|
||||
export type DiffChange = {
|
||||
key: string;
|
||||
label: string;
|
||||
oldValue: any;
|
||||
newValue: any;
|
||||
type: "created" | "updated" | "deleted" | "unchanged";
|
||||
typeLabel: "erstellt" | "geändert" | "gelöscht" | "unverändert";
|
||||
};
|
||||
|
||||
const IGNORED_KEYS = new Set([
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"id",
|
||||
"phases"
|
||||
]);
|
||||
|
||||
/**
|
||||
* Vergleicht zwei Objekte und gibt die Änderungen zurück.
|
||||
* @param obj1 Altes Objekt
|
||||
* @param obj2 Neues Objekt
|
||||
* @param ctx Lookup-Objekte (z. B. { projects, customers, vendors, profiles, plants })
|
||||
*/
|
||||
export function diffObjects(
|
||||
obj1: Record<string, any>,
|
||||
obj2: Record<string, any>,
|
||||
ctx: Record<string, any> = {}
|
||||
): DiffChange[] {
|
||||
const diffs: DiffChange[] = [];
|
||||
|
||||
const allKeys = new Set([
|
||||
...Object.keys(obj1 || {}),
|
||||
...Object.keys(obj2 || {}),
|
||||
]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (IGNORED_KEYS.has(key)) continue; // Felder überspringen
|
||||
|
||||
const oldVal = obj1?.[key];
|
||||
const newVal = obj2?.[key];
|
||||
|
||||
console.log(oldVal, key, newVal);
|
||||
|
||||
// Wenn beides null/undefined → ignorieren
|
||||
if (
|
||||
(oldVal === null || oldVal === undefined || oldVal === "" || JSON.stringify(oldVal) === "[]") &&
|
||||
(newVal === null || newVal === undefined || newVal === "" || JSON.stringify(newVal) === "[]")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let type: DiffChange["type"] = "unchanged";
|
||||
let typeLabel: DiffChange["typeLabel"] = "unverändert";
|
||||
if (oldVal === newVal) {
|
||||
type = "unchanged";
|
||||
typeLabel = "unverändert";
|
||||
} else if (oldVal === undefined) {
|
||||
type = "created";
|
||||
typeLabel = "erstellt"
|
||||
} else if (newVal === undefined) {
|
||||
type = "deleted";
|
||||
typeLabel = "gelöscht"
|
||||
} else {
|
||||
type = "updated";
|
||||
typeLabel = "geändert"
|
||||
}
|
||||
|
||||
if (type === "unchanged") continue;
|
||||
|
||||
const translation = diffTranslations[key];
|
||||
let label = key;
|
||||
let resolvedOld = oldVal;
|
||||
let resolvedNew = newVal;
|
||||
|
||||
if (translation) {
|
||||
label = translation.label;
|
||||
if (translation.resolve) {
|
||||
const { oldVal: resOld, newVal: resNew } = translation.resolve(
|
||||
oldVal,
|
||||
newVal,
|
||||
ctx
|
||||
);
|
||||
resolvedOld = resOld;
|
||||
resolvedNew = resNew;
|
||||
}
|
||||
}
|
||||
|
||||
diffs.push({
|
||||
key,
|
||||
label,
|
||||
typeLabel,
|
||||
oldValue: resolvedOld ?? "-",
|
||||
newValue: resolvedNew ?? "-",
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
return diffs;
|
||||
}
|
||||
165
backend/src/utils/diffTranslations.ts
Normal file
165
backend/src/utils/diffTranslations.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
type ValueResolver = (
|
||||
oldVal: any,
|
||||
newVal: any,
|
||||
ctx?: Record<string, any>
|
||||
) => { oldVal: any; newVal: any };
|
||||
|
||||
export const diffTranslations: Record<
|
||||
string,
|
||||
{ label: string; resolve?: ValueResolver }
|
||||
> = {
|
||||
project: {
|
||||
label: "Projekt",
|
||||
resolve: (o, n, ctx) => ({
|
||||
oldVal: o ? ctx?.projects?.find((i: any) => i.id === o)?.name ?? "-" : "-",
|
||||
newVal: n ? ctx?.projects?.find((i: any) => i.id === n)?.name ?? "-" : "-",
|
||||
}),
|
||||
},
|
||||
title: { label: "Titel" },
|
||||
type: { label: "Typ" },
|
||||
notes: { label: "Notizen" },
|
||||
link: { label: "Link" },
|
||||
|
||||
start: {
|
||||
label: "Start",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: o ? dayjs(o).format("DD.MM.YYYY HH:mm") : "-",
|
||||
newVal: n ? dayjs(n).format("DD.MM.YYYY HH:mm") : "-",
|
||||
}),
|
||||
},
|
||||
end: {
|
||||
label: "Ende",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: o ? dayjs(o).format("DD.MM.YYYY HH:mm") : "-",
|
||||
newVal: n ? dayjs(n).format("DD.MM.YYYY HH:mm") : "-",
|
||||
}),
|
||||
},
|
||||
birthday: {
|
||||
label: "Geburtstag",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: o ? dayjs(o).format("DD.MM.YYYY") : "-",
|
||||
newVal: n ? dayjs(n).format("DD.MM.YYYY") : "-",
|
||||
}),
|
||||
},
|
||||
resources: {
|
||||
label: "Resourcen",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: Array.isArray(o) ? o.map((i: any) => i.title).join(", ") : "-",
|
||||
newVal: Array.isArray(n) ? n.map((i: any) => i.title).join(", ") : "-",
|
||||
}),
|
||||
},
|
||||
|
||||
customerNumber: { label: "Kundennummer" },
|
||||
active: {
|
||||
label: "Aktiv",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: o === true ? "Aktiv" : "Gesperrt",
|
||||
newVal: n === true ? "Aktiv" : "Gesperrt",
|
||||
}),
|
||||
},
|
||||
isCompany: {
|
||||
label: "Firmenkunde",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: o === true ? "Firma" : "Privatkunde",
|
||||
newVal: n === true ? "Firma" : "Privatkunde",
|
||||
}),
|
||||
},
|
||||
special: { label: "Adresszusatz" },
|
||||
street: { label: "Straße & Hausnummer" },
|
||||
city: { label: "Ort" },
|
||||
zip: { label: "Postleitzahl" },
|
||||
country: { label: "Land" },
|
||||
web: { label: "Webseite" },
|
||||
email: { label: "E-Mail" },
|
||||
tel: { label: "Telefon" },
|
||||
ustid: { label: "USt-ID" },
|
||||
role: { label: "Rolle" },
|
||||
phoneHome: { label: "Festnetz" },
|
||||
phoneMobile: { label: "Mobiltelefon" },
|
||||
salutation: { label: "Anrede" },
|
||||
firstName: { label: "Vorname" },
|
||||
lastName: { label: "Nachname" },
|
||||
name: { label: "Name" },
|
||||
nameAddition: { label: "Name Zusatz" },
|
||||
approved: { label: "Genehmigt" },
|
||||
manufacturer: { label: "Hersteller" },
|
||||
purchasePrice: { label: "Kaufpreis" },
|
||||
purchaseDate: { label: "Kaufdatum" },
|
||||
serialNumber: { label: "Seriennummer" },
|
||||
usePlanning: { label: "In Plantafel verwenden" },
|
||||
currentSpace: { label: "Lagerplatz" },
|
||||
|
||||
customer: {
|
||||
label: "Kunde",
|
||||
resolve: (o, n, ctx) => ({
|
||||
oldVal: o ? ctx?.customers?.find((i: any) => i.id === o)?.name ?? "-" : "-",
|
||||
newVal: n ? ctx?.customers?.find((i: any) => i.id === n)?.name ?? "-" : "-",
|
||||
}),
|
||||
},
|
||||
vendor: {
|
||||
label: "Lieferant",
|
||||
resolve: (o, n, ctx) => ({
|
||||
oldVal: o ? ctx?.vendors?.find((i: any) => i.id === o)?.name ?? "-" : "-",
|
||||
newVal: n ? ctx?.vendors?.find((i: any) => i.id === n)?.name ?? "-" : "-",
|
||||
}),
|
||||
},
|
||||
|
||||
description: { label: "Beschreibung" },
|
||||
categorie: { label: "Kategorie" },
|
||||
|
||||
profile: {
|
||||
label: "Mitarbeiter",
|
||||
resolve: (o, n, ctx) => ({
|
||||
oldVal: o ? ctx?.profiles?.find((i: any) => i.id === o)?.fullName ?? "-" : "-",
|
||||
newVal: n ? ctx?.profiles?.find((i: any) => i.id === n)?.fullName ?? "-" : "-",
|
||||
}),
|
||||
},
|
||||
plant: {
|
||||
label: "Objekt",
|
||||
resolve: (o, n, ctx) => ({
|
||||
oldVal: o ? ctx?.plants?.find((i: any) => i.id === o)?.name ?? "-" : "-",
|
||||
newVal: n ? ctx?.plants?.find((i: any) => i.id === n)?.name ?? "-" : "-",
|
||||
}),
|
||||
},
|
||||
|
||||
annualPaidLeaveDays: { label: "Urlaubstage" },
|
||||
employeeNumber: { label: "Mitarbeiternummer" },
|
||||
weeklyWorkingDays: { label: "Wöchentliche Arbeitstage" },
|
||||
weeklyWorkingHours: { label: "Wöchentliche Arbeitszeit" },
|
||||
customerRef: { label: "Referenz des Kunden" },
|
||||
|
||||
licensePlate: { label: "Kennzeichen" },
|
||||
tankSize: { label: "Tankvolumen" },
|
||||
towingCapacity: { label: "Anhängelast" },
|
||||
color: { label: "Farbe" },
|
||||
customPaymentDays: { label: "Zahlungsziel in Tagen" },
|
||||
customSurchargePercentage: { label: "Individueller Aufschlag" },
|
||||
powerInKW: { label: "Leistung" },
|
||||
|
||||
driver: {
|
||||
label: "Fahrer",
|
||||
resolve: (o, n, ctx) => ({
|
||||
oldVal: o ? ctx?.profiles?.find((i: any) => i.id === o)?.fullName ?? "-" : "-",
|
||||
newVal: n ? ctx?.profiles?.find((i: any) => i.id === n)?.fullName ?? "-" : "-",
|
||||
}),
|
||||
},
|
||||
|
||||
projecttype: { label: "Projekttyp" },
|
||||
|
||||
fixed: {
|
||||
label: "Festgeschrieben",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: o === true ? "Ja" : "Nein",
|
||||
newVal: n === true ? "Ja" : "Nein",
|
||||
}),
|
||||
},
|
||||
archived: {
|
||||
label: "Archiviert",
|
||||
resolve: (o, n) => ({
|
||||
oldVal: o === true ? "Ja" : "Nein",
|
||||
newVal: n === true ? "Ja" : "Nein",
|
||||
}),
|
||||
},
|
||||
};
|
||||
45
backend/src/utils/emailengine.ts
Normal file
45
backend/src/utils/emailengine.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import axios from "axios"
|
||||
|
||||
const AxiosEE = axios.create({
|
||||
baseURL: process.env.EMAILENGINE_URL ||"https://ee.fedeo.io/v1",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.EMAILENGINE_TOKEN || "dcd8209bc5371c728f9ec951600afcfc74e8c391a7e984b2a6df9c4665dc7ad6"}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
export async function sendMailAsUser(
|
||||
to: string,
|
||||
subject: string,
|
||||
html: string,
|
||||
text: string,
|
||||
account: string,
|
||||
cc: string,
|
||||
bcc: string,
|
||||
attachments: any,
|
||||
): Promise<{ success: boolean; info?: any; error?: any }> {
|
||||
try {
|
||||
const sendData = {
|
||||
to: to.split(";").map(i => { return {address: i}}),
|
||||
cc: cc ? cc.split(";").map((i:any) => { return {address: i}}) : null,
|
||||
bcc: bcc ? bcc.split(";").map((i:any) => { return {address: i}}) : null,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
attachments
|
||||
}
|
||||
|
||||
if(sendData.cc === null) delete sendData.cc
|
||||
if(sendData.bcc === null) delete sendData.bcc
|
||||
|
||||
const {data} = await AxiosEE.post(`/account/${account}/submit`, sendData)
|
||||
|
||||
return { success: true, info: data }
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Mailversand:", err)
|
||||
return { success: false, error: err }
|
||||
}
|
||||
}
|
||||
388
backend/src/utils/export/datev.ts
Normal file
388
backend/src/utils/export/datev.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
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)
|
||||
|
||||
const getCreatedDocumentTotal = (item) => {
|
||||
let totalNet = 0
|
||||
let total19 = 0
|
||||
let total7 = 0
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let totalGross = Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
|
||||
|
||||
|
||||
|
||||
return {
|
||||
totalNet: totalNet,
|
||||
total19: total19,
|
||||
total7: total7,
|
||||
totalGross: totalGross,
|
||||
}
|
||||
}
|
||||
|
||||
const escapeString = (str) => {
|
||||
|
||||
str = (str ||"")
|
||||
.replaceAll("\n","")
|
||||
.replaceAll(";","")
|
||||
.replaceAll(/\r/g,"")
|
||||
.replaceAll(/"/g,"")
|
||||
.replaceAll(/ü/g,"ue")
|
||||
.replaceAll(/ä/g,"ae")
|
||||
.replaceAll(/ö/g,"oe")
|
||||
return str
|
||||
}
|
||||
|
||||
const displayCurrency = (input, onlyAbs = false) => {
|
||||
|
||||
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> {
|
||||
|
||||
try {
|
||||
const zipFileWriter = new BlobWriter()
|
||||
const zipWriter = new ZipWriter(zipFileWriter)
|
||||
|
||||
|
||||
|
||||
//Basic Information
|
||||
|
||||
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";;;"";""`
|
||||
|
||||
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`
|
||||
|
||||
//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()
|
||||
|
||||
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 {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 downloadFile = async (bucketName, filePath, downloadFilePath,fileId) => {
|
||||
|
||||
console.log(filePath)
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: filePath,
|
||||
})
|
||||
|
||||
const { Body, ContentType } = await s3.send(command)
|
||||
|
||||
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)
|
||||
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);
|
||||
}
|
||||
|
||||
let bookingLines = []
|
||||
|
||||
createddocuments.forEach(createddocument => {
|
||||
|
||||
let file = filesCreateddocuments.find(i => i.createddocument === createddocument.id);
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
let shSelector = "S"
|
||||
if(Math.sign(total) === 1) {
|
||||
shSelector = "S"
|
||||
} else if (Math.sign(total) === -1) {
|
||||
shSelector = "H"
|
||||
}
|
||||
|
||||
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;;;;"";;;;;;;`)
|
||||
|
||||
})
|
||||
|
||||
incominginvoices.forEach(incominginvoice => {
|
||||
console.log(incominginvoice.id);
|
||||
incominginvoice.accounts.forEach(account => {
|
||||
|
||||
let file = filesIncomingInvoices.find(i => i.incominginvoice === incominginvoice.id);
|
||||
|
||||
|
||||
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!');
|
||||
}
|
||||
});*/
|
||||
|
||||
|
||||
let obj = {
|
||||
archive: {
|
||||
'@version':"5.0",
|
||||
"@generatingSystem":"fedeo.de",
|
||||
"@xsi:schemaLocation":"http://xml.datev.de/bedi/tps/document/v05.0 Document_v050.xsd",
|
||||
"@xmlns":"http://xml.datev.de/bedi/tps/document/v05.0",
|
||||
"@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance",
|
||||
header: {
|
||||
date: dayjs().format("YYYY-MM-DDTHH:mm:ss")
|
||||
},
|
||||
content: {
|
||||
document: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filesCreateddocuments.forEach(file => {
|
||||
obj.archive.content.document.push({
|
||||
"@guid": file.id,
|
||||
extension: {
|
||||
"@xsi:type":"File",
|
||||
"@name":`${file.id}.pdf`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
filesIncomingInvoices.forEach(file => {
|
||||
obj.archive.content.document.push({
|
||||
"@guid": file.id,
|
||||
extension: {
|
||||
"@xsi:type":"File",
|
||||
"@name":`${file.id}.pdf`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true})
|
||||
|
||||
//console.log(doc.end({pretty: true}));
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
114
backend/src/utils/export/sepa.ts
Normal file
114
backend/src/utils/export/sepa.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import xmlbuilder from "xmlbuilder";
|
||||
import {randomUUID} from "node:crypto";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const createSEPAExport = async (server,idsToExport, tenant_id) => {
|
||||
const {data,error} = await server.supabase.from("createddocuments").select().eq("tenant", tenant_id).in("id", idsToExport)
|
||||
const {data:tenantData,error:tenantError} = await server.supabase.from("tenants").select().eq("id", tenant_id).single()
|
||||
console.log(tenantData)
|
||||
console.log(tenantError)
|
||||
|
||||
console.log(data)
|
||||
|
||||
let transactions = []
|
||||
|
||||
let obj = {
|
||||
Document: {
|
||||
'@xmlns':"urn:iso:std:iso:20022:tech:xsd:pain.008.001.02",
|
||||
'CstmrDrctDbtInitn': {
|
||||
'GrpHdr': {
|
||||
'MsgId': randomUUID(),
|
||||
'CredDtTm': dayjs().format("YYYY-MM-DDTHH:mm:ss"),
|
||||
'NbOfTxs': transactions.length,
|
||||
'CtrlSum': 0, // TODO: Total Sum
|
||||
'InitgPty': {
|
||||
'Nm': tenantData.name
|
||||
}
|
||||
},
|
||||
'PmtInf': {
|
||||
'PmtInfId': "", // TODO: Mandatsreferenz,
|
||||
'PmtMtd': "DD",
|
||||
'BtchBookg': "true", // TODO: BatchBooking,
|
||||
'NbOfTxs': transactions.length,
|
||||
'CtrlSum': 0, //TODO: Total Sum
|
||||
'PmtTpInf': {
|
||||
'SvcLvl': {
|
||||
'Cd': "SEPA"
|
||||
},
|
||||
'LclInstrm': {
|
||||
'Cd': "CORE" // Core für BASIS / B2B für Firmen
|
||||
},
|
||||
'SeqTp': "RCUR" // TODO: Unterscheidung Einmalig / Wiederkehrend
|
||||
},
|
||||
'ReqdColltnDt': dayjs().add(3, "days").format("YYYY-MM-DDTHH:mm:ss"),
|
||||
'Cdtr': {
|
||||
'Nm': tenantData.name
|
||||
},
|
||||
'CdtrAcct': {
|
||||
'Id': {
|
||||
'IBAN': "DE" // TODO: IBAN Gläubiger EINSETZEN
|
||||
}
|
||||
},
|
||||
'CdtrAgt': {
|
||||
'FinInstnId': {
|
||||
'BIC': "BIC" // TODO: BIC des Gläubigers einsetzen
|
||||
}
|
||||
},
|
||||
'ChrgBr': "SLEV",
|
||||
'CdtrSchmeId': {
|
||||
'Id': {
|
||||
'PrvtId': {
|
||||
'Othr': {
|
||||
'Id': tenantData.creditorId,
|
||||
'SchmeNm': {
|
||||
'Prty': "SEPA"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
//TODO ITERATE ALL INVOICES HERE
|
||||
'DrctDbtTxInf': {
|
||||
'PmtId': {
|
||||
'EndToEndId': "" // TODO: EINDEUTIGE ReFERENZ wahrscheinlich RE Nummer
|
||||
},
|
||||
'InstdAmt': {
|
||||
'@Ccy':"EUR",
|
||||
'#text':100 //TODO: Rechnungssumme zwei NK mit Punkt
|
||||
},
|
||||
'DrctDbtTx': {
|
||||
'MndtRltdInf': {
|
||||
'MndtId': "", // TODO: Mandatsref,
|
||||
'DtOfSgntr': "", //TODO: Unterschrieben am,
|
||||
'AmdmntInd': "" //TODO: Mandat geändert
|
||||
}
|
||||
},
|
||||
'DbtrAgt': {
|
||||
'FinInstnId': {
|
||||
'BIC': "", //TODO: BIC Debtor
|
||||
}
|
||||
},
|
||||
'Dbtr': {
|
||||
'Nm': "" // TODO NAME Debtor
|
||||
},
|
||||
'DbtrAcct': {
|
||||
'Id': {
|
||||
'IBAN': "DE" // TODO IBAN Debtor
|
||||
}
|
||||
},
|
||||
'RmtInf': {
|
||||
'Ustrd': "" //TODO Verwendungszweck Rechnungsnummer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true})
|
||||
|
||||
console.log(doc.end({pretty:true}))
|
||||
|
||||
}
|
||||
95
backend/src/utils/files.ts
Normal file
95
backend/src/utils/files.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { PutObjectCommand } from "@aws-sdk/client-s3"
|
||||
import { s3 } from "./s3"
|
||||
import { secrets } from "./secrets"
|
||||
|
||||
// Drizzle schema
|
||||
import { files } from "../../db/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { FastifyInstance } from "fastify"
|
||||
|
||||
export const saveFile = async (
|
||||
server: FastifyInstance,
|
||||
tenant: number,
|
||||
messageId: string | number | null, // Typ angepasst (oft null bei manueller Gen)
|
||||
attachment: any, // Kann File, Buffer oder Mailparser-Objekt sein
|
||||
folder: string | null,
|
||||
type: string | null,
|
||||
other: Record<string, any> = {}
|
||||
) => {
|
||||
try {
|
||||
// ---------------------------------------------------
|
||||
// 1️⃣ FILE ENTRY ANLEGEN
|
||||
// ---------------------------------------------------
|
||||
const insertRes = await server.db
|
||||
.insert(files)
|
||||
.values({
|
||||
tenant,
|
||||
folder,
|
||||
type,
|
||||
...other
|
||||
})
|
||||
.returning()
|
||||
|
||||
const created = insertRes?.[0]
|
||||
if (!created) {
|
||||
console.error("File creation failed (no row returned)")
|
||||
return null
|
||||
}
|
||||
|
||||
// Name ermitteln (Fallback Logik)
|
||||
// Wenn attachment ein Buffer ist, muss der Name in 'other' stehen oder generiert werden
|
||||
const filename = attachment.filename || other.filename || `${created.id}.pdf`
|
||||
|
||||
// ---------------------------------------------------
|
||||
// 2️⃣ BODY & CONTENT TYPE ERMITTELN
|
||||
// ---------------------------------------------------
|
||||
let body: Buffer | Uint8Array | string
|
||||
let contentType = type || "application/octet-stream"
|
||||
|
||||
if (Buffer.isBuffer(attachment)) {
|
||||
// FALL 1: RAW BUFFER (von finishManualGeneration)
|
||||
body = attachment
|
||||
// ContentType wurde oben schon über 'type' Parameter gesetzt (z.B. application/pdf)
|
||||
} else if (typeof File !== "undefined" && attachment instanceof File) {
|
||||
// FALL 2: BROWSER FILE
|
||||
body = Buffer.from(await attachment.arrayBuffer())
|
||||
contentType = attachment.type || contentType
|
||||
} else if (attachment.content) {
|
||||
// FALL 3: MAILPARSER OBJECT
|
||||
body = attachment.content
|
||||
contentType = attachment.contentType || contentType
|
||||
} else {
|
||||
console.error("saveFile: Unknown attachment format")
|
||||
return null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------
|
||||
// 3️⃣ S3 UPLOAD
|
||||
// ---------------------------------------------------
|
||||
const key = `${tenant}/filesbyid/${created.id}/${filename}`
|
||||
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType,
|
||||
ContentLength: body.length // <--- WICHTIG: Behebt den AWS Fehler
|
||||
})
|
||||
)
|
||||
|
||||
// ---------------------------------------------------
|
||||
// 4️⃣ PATH IN DB SETZEN
|
||||
// ---------------------------------------------------
|
||||
await server.db
|
||||
.update(files)
|
||||
.set({ path: key })
|
||||
.where(eq(files.id, created.id))
|
||||
|
||||
console.log(`File saved: ${key}`)
|
||||
return { id: created.id, key }
|
||||
} catch (err) {
|
||||
console.error("saveFile error:", err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
174
backend/src/utils/functions.ts
Normal file
174
backend/src/utils/functions.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import {FastifyInstance} from "fastify";
|
||||
// import { PNG } from 'pngjs'
|
||||
// import { ready as zplReady } from 'zpl-renderer-js'
|
||||
// import { Utils } from '@mmote/niimbluelib'
|
||||
// import { createCanvas } from 'canvas'
|
||||
// import bwipjs from 'bwip-js'
|
||||
// import Sharp from 'sharp'
|
||||
// import fs from 'fs'
|
||||
|
||||
import { tenants } from "../../db/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
export const useNextNumberRangeNumber = async (
|
||||
server: FastifyInstance,
|
||||
tenantId: number,
|
||||
numberRange: string
|
||||
) => {
|
||||
// 1️⃣ Tenant laden
|
||||
const [tenant] = await server.db
|
||||
.select()
|
||||
.from(tenants)
|
||||
.where(eq(tenants.id, tenantId))
|
||||
|
||||
if (!tenant) {
|
||||
throw new Error(`Tenant ${tenantId} not found`)
|
||||
}
|
||||
|
||||
const numberRanges = tenant.numberRanges || {}
|
||||
|
||||
if (!numberRanges[numberRange]) {
|
||||
throw new Error(`Number range '${numberRange}' not found`)
|
||||
}
|
||||
|
||||
const current = numberRanges[numberRange]
|
||||
|
||||
// 2️⃣ Used Number generieren
|
||||
const usedNumber =
|
||||
(current.prefix || "") +
|
||||
current.nextNumber +
|
||||
(current.suffix || "")
|
||||
|
||||
// 3️⃣ nextNumber erhöhen
|
||||
const updatedRanges = {
|
||||
// @ts-ignore
|
||||
...numberRanges,
|
||||
[numberRange]: {
|
||||
...current,
|
||||
nextNumber: current.nextNumber + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 4️⃣ Tenant aktualisieren
|
||||
await server.db
|
||||
.update(tenants)
|
||||
.set({ numberRanges: updatedRanges })
|
||||
.where(eq(tenants.id, tenantId))
|
||||
|
||||
return { usedNumber }
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
|
||||
// 1️⃣ PNG dekodieren
|
||||
const buffer = Buffer.from(base64Png, 'base64')
|
||||
const png = PNG.sync.read(buffer) // liefert {width, height, data: Uint8Array(RGBA)}
|
||||
|
||||
const { width, height, data } = png
|
||||
console.log(width, height, data)
|
||||
const cols = printDirection === 'left' ? height : width
|
||||
const rows = printDirection === 'left' ? width : height
|
||||
const rowsData = []
|
||||
|
||||
console.log(cols)
|
||||
|
||||
if (cols % 8 !== 0) throw new Error('Column count must be multiple of 8')
|
||||
|
||||
// 2️⃣ Zeilenweise durchgehen und Bits bilden
|
||||
for (let row = 0; row < rows; row++) {
|
||||
let isVoid = true
|
||||
let blackPixelsCount = 0
|
||||
const rowData = new Uint8Array(cols / 8)
|
||||
|
||||
for (let colOct = 0; colOct < cols / 8; colOct++) {
|
||||
let pixelsOctet = 0
|
||||
for (let colBit = 0; colBit < 8; colBit++) {
|
||||
const x = printDirection === 'left' ? row : colOct * 8 + colBit
|
||||
const y = printDirection === 'left' ? height - 1 - (colOct * 8 + colBit) : row
|
||||
const idx = (y * width + x) * 4
|
||||
const lum = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]
|
||||
const isBlack = lum < 128
|
||||
if (isBlack) {
|
||||
pixelsOctet |= 1 << (7 - colBit)
|
||||
isVoid = false
|
||||
blackPixelsCount++
|
||||
}
|
||||
}
|
||||
rowData[colOct] = pixelsOctet
|
||||
}
|
||||
|
||||
const newPart = {
|
||||
dataType: isVoid ? 'void' : 'pixels',
|
||||
rowNumber: row,
|
||||
repeat: 1,
|
||||
rowData: isVoid ? undefined : rowData,
|
||||
blackPixelsCount,
|
||||
}
|
||||
|
||||
if (rowsData.length === 0) {
|
||||
rowsData.push(newPart)
|
||||
} else {
|
||||
const last = rowsData[rowsData.length - 1]
|
||||
let same = newPart.dataType === last.dataType
|
||||
if (same && newPart.dataType === 'pixels') {
|
||||
same = Utils.u8ArraysEqual(newPart.rowData, last.rowData)
|
||||
}
|
||||
if (same) last.repeat++
|
||||
else rowsData.push(newPart)
|
||||
if (row % 200 === 199) {
|
||||
rowsData.push({
|
||||
dataType: 'check',
|
||||
rowNumber: row,
|
||||
repeat: 0,
|
||||
rowData: undefined,
|
||||
blackPixelsCount: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { cols, rows, rowsData }
|
||||
}
|
||||
|
||||
export async function generateLabel(context,width,height) {
|
||||
// Canvas für Hintergrund & Text
|
||||
const canvas = createCanvas(width, height)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Hintergrund weiß
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// Überschrift
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.font = '32px Arial'
|
||||
ctx.fillText(context.text, 20, 40)
|
||||
|
||||
// 3) DataMatrix
|
||||
const dataMatrixPng = await bwipjs.toBuffer({
|
||||
bcid: 'datamatrix',
|
||||
text: context.datamatrix,
|
||||
scale: 6,
|
||||
})
|
||||
|
||||
// Basisbild aus Canvas
|
||||
const base = await Sharp(canvas.toBuffer())
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
// Alles zusammen compositen
|
||||
const final = await Sharp(base)
|
||||
.composite([
|
||||
{ input: dataMatrixPng, top: 60, left: 20 },
|
||||
])
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
fs.writeFileSync('label.png', final)
|
||||
|
||||
// Optional: Base64 zurückgeben (z.B. für API)
|
||||
const base64 = final.toString('base64')
|
||||
|
||||
return base64
|
||||
}*/
|
||||
204
backend/src/utils/gpt.ts
Normal file
204
backend/src/utils/gpt.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import dayjs from "dayjs";
|
||||
import axios from "axios";
|
||||
import OpenAI from "openai";
|
||||
import { z } from "zod";
|
||||
import { zodResponseFormat } from "openai/helpers/zod";
|
||||
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { Blob } from "buffer";
|
||||
import { FastifyInstance } from "fastify";
|
||||
|
||||
import { s3 } from "./s3";
|
||||
import { secrets } from "./secrets";
|
||||
|
||||
// Drizzle schema
|
||||
import { vendors, accounts } from "../../db/schema";
|
||||
import {eq} from "drizzle-orm";
|
||||
|
||||
let openai: OpenAI | null = null;
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// INITIALIZE OPENAI
|
||||
// ---------------------------------------------------------
|
||||
export const initOpenAi = async () => {
|
||||
openai = new OpenAI({
|
||||
apiKey: secrets.OPENAI_API_KEY,
|
||||
});
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// STREAM → BUFFER
|
||||
// ---------------------------------------------------------
|
||||
async function streamToBuffer(stream: any): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// GPT RESPONSE FORMAT (Zod Schema)
|
||||
// ---------------------------------------------------------
|
||||
const InstructionFormat = z.object({
|
||||
invoice_number: z.string(),
|
||||
invoice_date: z.string(),
|
||||
invoice_duedate: z.string(),
|
||||
invoice_type: z.string(),
|
||||
delivery_type: z.string(),
|
||||
delivery_note_number: z.string(),
|
||||
reference: z.string(),
|
||||
issuer: z.object({
|
||||
id: z.number().nullable().optional(),
|
||||
name: z.string(),
|
||||
address: z.string(),
|
||||
phone: z.string(),
|
||||
email: z.string(),
|
||||
bank: z.string(),
|
||||
bic: z.string(),
|
||||
iban: z.string(),
|
||||
}),
|
||||
recipient: z.object({
|
||||
name: z.string(),
|
||||
address: z.string(),
|
||||
phone: z.string(),
|
||||
email: z.string(),
|
||||
}),
|
||||
invoice_items: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
unit: z.string(),
|
||||
quantity: z.number(),
|
||||
total: z.number(),
|
||||
total_without_tax: z.number(),
|
||||
tax_rate: z.number(),
|
||||
ean: z.number().nullable().optional(),
|
||||
article_number: z.number().nullable().optional(),
|
||||
account_number: z.number().nullable().optional(),
|
||||
account_id: z.number().nullable().optional(),
|
||||
})
|
||||
),
|
||||
subtotal: z.number(),
|
||||
tax_rate: z.number(),
|
||||
tax: z.number(),
|
||||
total: z.number(),
|
||||
terms: z.string(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MAIN FUNCTION – REPLACES SUPABASE VERSION
|
||||
// ---------------------------------------------------------
|
||||
export const getInvoiceDataFromGPT = async function (
|
||||
server: FastifyInstance,
|
||||
file: any,
|
||||
tenantId: number
|
||||
) {
|
||||
await initOpenAi();
|
||||
|
||||
if (!openai) {
|
||||
throw new Error("OpenAI not initialized. Call initOpenAi() first.");
|
||||
}
|
||||
|
||||
console.log(`📄 Reading invoice file ${file.id}`);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 1) DOWNLOAD PDF FROM S3
|
||||
// ---------------------------------------------------------
|
||||
let fileData: Buffer;
|
||||
|
||||
try {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: file.path,
|
||||
});
|
||||
|
||||
const response: any = await s3.send(command);
|
||||
fileData = await streamToBuffer(response.Body);
|
||||
} catch (err) {
|
||||
console.log(`❌ S3 Download failed for file ${file.id}`, err);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only process PDFs
|
||||
if (!file.path.toLowerCase().endsWith(".pdf")) {
|
||||
server.log.warn(`Skipping non-PDF file ${file.id}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileBlob = new Blob([fileData], { type: "application/pdf" });
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 2) SEND FILE TO PDF → TEXT API
|
||||
// ---------------------------------------------------------
|
||||
const form = new FormData();
|
||||
form.append("fileInput", fileBlob, file.path.split("/").pop());
|
||||
form.append("outputFormat", "txt");
|
||||
|
||||
let extractedText: string;
|
||||
|
||||
try {
|
||||
const res = await axios.post(
|
||||
"http://23.88.52.85:8080/api/v1/convert/pdf/text",
|
||||
form,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
Authorization: `Bearer ${secrets.STIRLING_API_KEY}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
extractedText = res.data;
|
||||
} catch (err) {
|
||||
console.log("❌ PDF OCR API failed", err);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 3) LOAD VENDORS + ACCOUNTS (DRIZZLE)
|
||||
// ---------------------------------------------------------
|
||||
const vendorList = await server.db
|
||||
.select({ id: vendors.id, name: vendors.name })
|
||||
.from(vendors)
|
||||
.where(eq(vendors.tenant,tenantId));
|
||||
|
||||
const accountList = await server.db
|
||||
.select({
|
||||
id: accounts.id,
|
||||
label: accounts.label,
|
||||
number: accounts.number,
|
||||
})
|
||||
.from(accounts);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 4) GPT ANALYSIS
|
||||
// ---------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
const completion = await openai.chat.completions.parse({
|
||||
model: "gpt-4o",
|
||||
store: true,
|
||||
response_format: zodResponseFormat(InstructionFormat as any, "instruction"),
|
||||
messages: [
|
||||
{ role: "user", content: extractedText },
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"You extract structured invoice data.\n\n" +
|
||||
`VENDORS: ${JSON.stringify(vendorList)}\n` +
|
||||
`ACCOUNTS: ${JSON.stringify(accountList)}\n\n` +
|
||||
"Match issuer by name to vendor.id.\n" +
|
||||
"Match invoice items to account id based on label/number.\n" +
|
||||
"Convert dates to YYYY-MM-DD.\n" +
|
||||
"Keep invoice items in original order.\n",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const parsed = completion.choices[0].message.parsed;
|
||||
|
||||
console.log(`🧾 Extracted invoice data for file ${file.id}`);
|
||||
|
||||
return parsed;
|
||||
};
|
||||
106
backend/src/utils/helpers.ts
Normal file
106
backend/src/utils/helpers.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// 🔧 Hilfsfunktionen
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { eq, ilike, and } from "drizzle-orm"
|
||||
|
||||
import { contacts, customers } from "../../db/schema"
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Extract Domain
|
||||
// -------------------------------------------------------------
|
||||
export function extractDomain(email: string) {
|
||||
if (!email) return null
|
||||
const parts = email.split("@")
|
||||
return parts.length === 2 ? parts[1].toLowerCase() : null
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Kunde oder Kontakt anhand E-Mail oder Domain finden
|
||||
// -------------------------------------------------------------
|
||||
export async function findCustomerOrContactByEmailOrDomain(
|
||||
server: FastifyInstance,
|
||||
fromMail: string,
|
||||
tenantId: number
|
||||
) {
|
||||
const sender = fromMail.toLowerCase()
|
||||
const senderDomain = extractDomain(sender)
|
||||
if (!senderDomain) return null
|
||||
|
||||
// 1️⃣ Direkter Match über Contacts (email)
|
||||
const contactMatch = await server.db
|
||||
.select({
|
||||
id: contacts.id,
|
||||
customer: contacts.customer,
|
||||
})
|
||||
.from(contacts)
|
||||
.where(
|
||||
and(
|
||||
eq(contacts.email, sender),
|
||||
eq(contacts.tenant, tenantId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (contactMatch.length && contactMatch[0].customer) {
|
||||
return {
|
||||
customer: contactMatch[0].customer,
|
||||
contact: contactMatch[0].id,
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Kunden anhand Domain vergleichen
|
||||
const allCustomers = await server.db
|
||||
.select({
|
||||
id: customers.id,
|
||||
infoData: customers.infoData,
|
||||
})
|
||||
.from(customers)
|
||||
.where(eq(customers.tenant, tenantId))
|
||||
|
||||
for (const c of allCustomers) {
|
||||
const info = c.infoData || {}
|
||||
|
||||
// @ts-ignore
|
||||
const email = info.email?.toLowerCase()
|
||||
//@ts-ignore
|
||||
const invoiceEmail = info.invoiceEmail?.toLowerCase()
|
||||
const emailDomain = extractDomain(email)
|
||||
const invoiceDomain = extractDomain(invoiceEmail)
|
||||
|
||||
if (
|
||||
sender === email ||
|
||||
sender === invoiceEmail ||
|
||||
senderDomain === emailDomain ||
|
||||
senderDomain === invoiceDomain
|
||||
) {
|
||||
return { customer: c.id, contact: null }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// getNestedValue (für Sortierung & Suche im Backend)
|
||||
// -------------------------------------------------------------
|
||||
export function getNestedValue(obj: any, path: string): any {
|
||||
return path
|
||||
.split(".")
|
||||
.reduce((acc, part) => (acc && acc[part] !== undefined ? acc[part] : undefined), obj)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// compareValues (Sortierung für paginated)
|
||||
// -------------------------------------------------------------
|
||||
export function compareValues(a: any, b: any): number {
|
||||
if (a === b) return 0
|
||||
if (a == null) return 1
|
||||
if (b == null) return -1
|
||||
|
||||
// String Compare
|
||||
if (typeof a === "string" && typeof b === "string") {
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
|
||||
// Numerisch
|
||||
return a < b ? -1 : 1
|
||||
}
|
||||
70
backend/src/utils/history.ts
Normal file
70
backend/src/utils/history.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
|
||||
export async function insertHistoryItem(
|
||||
server: FastifyInstance,
|
||||
params: {
|
||||
tenant_id: number
|
||||
created_by: string | null
|
||||
entity: string
|
||||
entityId: string | number
|
||||
action: "created" | "updated" | "unchanged" | "deleted" | "archived"
|
||||
oldVal?: any
|
||||
newVal?: any
|
||||
text?: string
|
||||
}
|
||||
) {
|
||||
const textMap = {
|
||||
created: `Neuer Eintrag in ${params.entity} erstellt`,
|
||||
updated: `Eintrag in ${params.entity} geändert`,
|
||||
archived: `Eintrag in ${params.entity} archiviert`,
|
||||
deleted: `Eintrag in ${params.entity} gelöscht`
|
||||
}
|
||||
|
||||
const columnMap: Record<string, string> = {
|
||||
customers: "customer",
|
||||
vendors: "vendor",
|
||||
projects: "project",
|
||||
plants: "plant",
|
||||
contacts: "contact",
|
||||
inventoryitems: "inventoryitem",
|
||||
products: "product",
|
||||
profiles: "profile",
|
||||
absencerequests: "absencerequest",
|
||||
events: "event",
|
||||
tasks: "task",
|
||||
vehicles: "vehicle",
|
||||
costcentres: "costcentre",
|
||||
ownaccounts: "ownaccount",
|
||||
documentboxes: "documentbox",
|
||||
hourrates: "hourrate",
|
||||
services: "service",
|
||||
roles: "role",
|
||||
checks: "check",
|
||||
spaces: "space",
|
||||
trackingtrips: "trackingtrip",
|
||||
createddocuments: "createddocument",
|
||||
inventoryitemgroups: "inventoryitemgroup",
|
||||
bankstatements: "bankstatement"
|
||||
}
|
||||
|
||||
const fkColumn = columnMap[params.entity]
|
||||
if (!fkColumn) {
|
||||
server.log.warn(`Keine History-Spalte für Entity: ${params.entity}`)
|
||||
return
|
||||
}
|
||||
|
||||
const entry = {
|
||||
tenant: params.tenant_id,
|
||||
created_by: params.created_by,
|
||||
text: params.text || textMap[params.action],
|
||||
action: params.action,
|
||||
[fkColumn]: params.entityId,
|
||||
oldVal: params.oldVal ? JSON.stringify(params.oldVal) : null,
|
||||
newVal: params.newVal ? JSON.stringify(params.newVal) : null
|
||||
}
|
||||
|
||||
const { error } = await server.supabase.from("historyitems").insert([entry])
|
||||
if (error) { // @ts-ignore
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
37
backend/src/utils/mailer.ts
Normal file
37
backend/src/utils/mailer.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import nodemailer from "nodemailer"
|
||||
import {secrets} from "./secrets"
|
||||
|
||||
export let transporter = null
|
||||
export const initMailer = async () => {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: secrets.MAILER_SMTP_HOST,
|
||||
port: Number(secrets.MAILER_SMTP_PORT) || 587,
|
||||
secure: secrets.MAILER_SMTP_SSL === "true", // true für 465, false für andere Ports
|
||||
auth: {
|
||||
user: secrets.MAILER_SMTP_USER,
|
||||
pass: secrets.MAILER_SMTP_PASS,
|
||||
},
|
||||
})
|
||||
console.log("Mailer Initialized!")
|
||||
}
|
||||
|
||||
export async function sendMail(
|
||||
to: string,
|
||||
subject: string,
|
||||
html: string
|
||||
): Promise<{ success: boolean; info?: any; error?: any }> {
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: secrets.MAILER_FROM,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
})
|
||||
|
||||
// Nodemailer liefert eine Info-Response zurück
|
||||
return { success: true, info }
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Mailversand:", err)
|
||||
return { success: false, error: err }
|
||||
}
|
||||
}
|
||||
15
backend/src/utils/password.ts
Normal file
15
backend/src/utils/password.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import bcrypt from "bcrypt"
|
||||
|
||||
export function generateRandomPassword(length = 12): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"
|
||||
let password = ""
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return password
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 10
|
||||
return bcrypt.hash(password, saltRounds)
|
||||
}
|
||||
1126
backend/src/utils/pdf.ts
Normal file
1126
backend/src/utils/pdf.ts
Normal file
File diff suppressed because it is too large
Load Diff
174
backend/src/utils/resource.config.ts
Normal file
174
backend/src/utils/resource.config.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
accounts,
|
||||
bankaccounts,
|
||||
bankrequisitions,
|
||||
bankstatements,
|
||||
contacts,
|
||||
contracts,
|
||||
costcentres,
|
||||
createddocuments,
|
||||
customers,
|
||||
files,
|
||||
filetags,
|
||||
folders,
|
||||
hourrates,
|
||||
incominginvoices,
|
||||
inventoryitemgroups,
|
||||
inventoryitems,
|
||||
letterheads,
|
||||
ownaccounts,
|
||||
plants,
|
||||
productcategories,
|
||||
products,
|
||||
projects,
|
||||
projecttypes,
|
||||
serialExecutions,
|
||||
servicecategories,
|
||||
services,
|
||||
spaces,
|
||||
statementallocations,
|
||||
tasks,
|
||||
texttemplates,
|
||||
units,
|
||||
vehicles,
|
||||
vendors
|
||||
} from "../../db/schema";
|
||||
|
||||
export const resourceConfig = {
|
||||
projects: {
|
||||
searchColumns: ["name"],
|
||||
mtoLoad: ["customer","plant","contract","projecttype"],
|
||||
mtmLoad: ["tasks", "files","createddocuments"],
|
||||
table: projects,
|
||||
numberRangeHolder: "projectNumber"
|
||||
},
|
||||
customers: {
|
||||
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
||||
mtmLoad: ["contacts","projects","plants","createddocuments","contracts"],
|
||||
table: customers,
|
||||
numberRangeHolder: "customerNumber",
|
||||
},
|
||||
contacts: {
|
||||
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
|
||||
table: contacts,
|
||||
mtoLoad: ["customer","vendor"]
|
||||
},
|
||||
contracts: {
|
||||
table: contracts,
|
||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
|
||||
numberRangeHolder: "contractNumber",
|
||||
},
|
||||
plants: {
|
||||
table: plants,
|
||||
mtoLoad: ["customer"],
|
||||
mtmLoad: ["projects","tasks","files"],
|
||||
},
|
||||
projecttypes: {
|
||||
table: projecttypes
|
||||
},
|
||||
vendors: {
|
||||
table: vendors,
|
||||
searchColumns: ["name","vendorNumber","notes","defaultPaymentType"],
|
||||
numberRangeHolder: "vendorNumber",
|
||||
},
|
||||
files: {
|
||||
table: files
|
||||
},
|
||||
folders: {
|
||||
table: folders
|
||||
},
|
||||
filetags: {
|
||||
table: filetags
|
||||
},
|
||||
inventoryitems: {
|
||||
table: inventoryitems,
|
||||
numberRangeHolder: "articleNumber",
|
||||
},
|
||||
inventoryitemgroups: {
|
||||
table: inventoryitemgroups
|
||||
},
|
||||
products: {
|
||||
table: products,
|
||||
searchColumns: ["name","manufacturer","ean","barcode","description","manfacturer_number","article_number"],
|
||||
},
|
||||
productcategories: {
|
||||
table: productcategories
|
||||
},
|
||||
services: {
|
||||
table: services,
|
||||
mtoLoad: ["unit"],
|
||||
searchColumns: ["name","description"],
|
||||
},
|
||||
servicecategories: {
|
||||
table: servicecategories
|
||||
},
|
||||
units: {
|
||||
table: units,
|
||||
},
|
||||
vehicles: {
|
||||
table: vehicles,
|
||||
searchColumns: ["name","license_plate","vin","color"],
|
||||
},
|
||||
hourrates: {
|
||||
table: hourrates,
|
||||
searchColumns: ["name"],
|
||||
},
|
||||
spaces: {
|
||||
table: spaces,
|
||||
searchColumns: ["name","space_number","type","info_data"],
|
||||
numberRangeHolder: "spaceNumber",
|
||||
},
|
||||
ownaccounts: {
|
||||
table: ownaccounts,
|
||||
searchColumns: ["name","description","number"],
|
||||
},
|
||||
costcentres: {
|
||||
table: costcentres,
|
||||
searchColumns: ["name","number","description"],
|
||||
mtoLoad: ["vehicle","project","inventoryitem"],
|
||||
numberRangeHolder: "number",
|
||||
},
|
||||
tasks: {
|
||||
table: tasks,
|
||||
},
|
||||
letterheads: {
|
||||
table: letterheads,
|
||||
|
||||
},
|
||||
createddocuments: {
|
||||
table: createddocuments,
|
||||
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument"],
|
||||
mtmLoad: ["statementallocations","files","createddocuments"],
|
||||
mtmListLoad: ["statementallocations"],
|
||||
},
|
||||
texttemplates: {
|
||||
table: texttemplates
|
||||
},
|
||||
incominginvoices: {
|
||||
table: incominginvoices,
|
||||
mtmLoad: ["statementallocations","files"],
|
||||
mtmListLoad: ["statementallocations"],
|
||||
mtoLoad: ["vendor"],
|
||||
},
|
||||
statementallocations: {
|
||||
table: statementallocations,
|
||||
mtoLoad: ["customer","vendor","incominginvoice","createddocument","ownaccount","bankstatement"]
|
||||
},
|
||||
accounts: {
|
||||
table: accounts,
|
||||
},
|
||||
bankstatements: {
|
||||
table: bankstatements,
|
||||
mtmListLoad: ["statementallocations"],
|
||||
mtmLoad: ["statementallocations"],
|
||||
},
|
||||
bankaccounts: {
|
||||
table: bankaccounts,
|
||||
},
|
||||
bankrequisitions: {
|
||||
table: bankrequisitions,
|
||||
},
|
||||
serialexecutions: {
|
||||
table: serialExecutions
|
||||
}
|
||||
}
|
||||
18
backend/src/utils/s3.ts
Normal file
18
backend/src/utils/s3.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
|
||||
import {secrets} from "./secrets";
|
||||
|
||||
|
||||
|
||||
export let s3 = null
|
||||
|
||||
export const initS3 = async () => {
|
||||
s3 = new S3Client({
|
||||
endpoint: secrets.S3_ENDPOINT, // z. B. http://localhost:9000 für MinIO
|
||||
region: secrets.S3_REGION,
|
||||
credentials: {
|
||||
accessKeyId: secrets.S3_ACCESS_KEY,
|
||||
secretAccessKey: secrets.S3_SECRET_KEY,
|
||||
},
|
||||
forcePathStyle: true, // wichtig für MinIO
|
||||
})
|
||||
}
|
||||
63
backend/src/utils/secrets.ts
Normal file
63
backend/src/utils/secrets.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {InfisicalSDK} from "@infisical/sdk"
|
||||
|
||||
const client = new InfisicalSDK({
|
||||
siteUrl: "https://secrets.fedeo.io"
|
||||
})
|
||||
|
||||
|
||||
|
||||
export let secrets = {
|
||||
|
||||
} as {
|
||||
COOKIE_SECRET: string
|
||||
JWT_SECRET: string
|
||||
PORT: number
|
||||
HOST: string
|
||||
DATABASE_URL: string
|
||||
SUPABASE_URL: string
|
||||
SUPABASE_SERVICE_ROLE_KEY: string
|
||||
S3_BUCKET: string
|
||||
ENCRYPTION_KEY: string
|
||||
MAILER_SMTP_HOST: string
|
||||
MAILER_SMTP_PORT: number
|
||||
MAILER_SMTP_SSL: string
|
||||
MAILER_SMTP_USER: string
|
||||
MAILER_SMTP_PASS: string
|
||||
MAILER_FROM: string
|
||||
S3_ENDPOINT: string
|
||||
S3_REGION: string
|
||||
S3_ACCESS_KEY: string
|
||||
S3_SECRET_KEY: string
|
||||
M2M_API_KEY: string
|
||||
API_BASE_URL: string
|
||||
GOCARDLESS_BASE_URL: string
|
||||
GOCARDLESS_SECRET_ID: string
|
||||
GOCARDLESS_SECRET_KEY: string
|
||||
DOKUBOX_IMAP_HOST: string
|
||||
DOKUBOX_IMAP_PORT: number
|
||||
DOKUBOX_IMAP_SECURE: boolean
|
||||
DOKUBOX_IMAP_USER: string
|
||||
DOKUBOX_IMAP_PASSWORD: string
|
||||
OPENAI_API_KEY: string
|
||||
STIRLING_API_KEY: string
|
||||
}
|
||||
|
||||
export async function loadSecrets () {
|
||||
|
||||
await client.auth().universalAuth.login({
|
||||
clientId: process.env.INFISICAL_CLIENT_ID,
|
||||
clientSecret: process.env.INFISICAL_CLIENT_SECRET,
|
||||
});
|
||||
|
||||
const allSecrets = await client.secrets().listSecrets({
|
||||
environment: "dev", // stg, dev, prod, or custom environment slugs
|
||||
projectId: "39774094-2aaf-49fb-a213-d6b2c10f6144"
|
||||
});
|
||||
|
||||
allSecrets.secrets.forEach(secret => {
|
||||
secrets[secret.secretKey] = secret.secretValue
|
||||
})
|
||||
console.log("✅ Secrets aus Infisical geladen");
|
||||
console.log(Object.keys(secrets).length + " Stück")
|
||||
}
|
||||
|
||||
40
backend/src/utils/sort.ts
Normal file
40
backend/src/utils/sort.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Sortiert ein Array von Objekten anhand einer Spalte.
|
||||
*
|
||||
* @param data Array von Objekten
|
||||
* @param column Sortierspalte (Property-Name im Objekt)
|
||||
* @param ascending true = aufsteigend, false = absteigend
|
||||
*/
|
||||
export function sortData<T extends Record<string, any>>(
|
||||
data: T[],
|
||||
column?: keyof T | null,
|
||||
ascending: boolean = true
|
||||
): T[] {
|
||||
if (!column) return data
|
||||
|
||||
return [...data].sort((a, b) => {
|
||||
const valA = a[column]
|
||||
const valB = b[column]
|
||||
|
||||
// null/undefined nach hinten
|
||||
if (valA == null && valB != null) return 1
|
||||
if (valB == null && valA != null) return -1
|
||||
if (valA == null && valB == null) return 0
|
||||
|
||||
// Zahlenvergleich
|
||||
if (typeof valA === "number" && typeof valB === "number") {
|
||||
return ascending ? valA - valB : valB - valA
|
||||
}
|
||||
|
||||
// Datumsvergleich
|
||||
// @ts-ignore
|
||||
if (valA instanceof Date && valB instanceof Date) {
|
||||
return ascending ? valA.getTime() - valB.getTime() : valB.getTime() - valA.getTime()
|
||||
}
|
||||
|
||||
// Fallback: Stringvergleich
|
||||
return ascending
|
||||
? String(valA).localeCompare(String(valB))
|
||||
: String(valB).localeCompare(String(valA))
|
||||
})
|
||||
}
|
||||
51
backend/src/utils/stringRendering.ts
Normal file
51
backend/src/utils/stringRendering.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
export const renderAsCurrency = (value: string | number,currencyString = "€") => {
|
||||
return `${Number(value).toFixed(2).replace(".",",")} ${currencyString}`
|
||||
}
|
||||
|
||||
export const splitStringBySpace = (input:string,maxSplitLength:number,removeLinebreaks = false) => {
|
||||
|
||||
if(removeLinebreaks) {
|
||||
input = input.replaceAll("\n","")
|
||||
}
|
||||
|
||||
let splitStrings: string[] = []
|
||||
|
||||
input.split("\n").forEach(string => {
|
||||
splitStrings.push(string)
|
||||
})
|
||||
|
||||
let returnSplitStrings: string[] = []
|
||||
|
||||
splitStrings.forEach(string => {
|
||||
let regex = / /gi, result, indices = [];
|
||||
while ( (result = regex.exec(string)) ) {
|
||||
indices.push(result.index);
|
||||
}
|
||||
|
||||
let lastIndex = 0
|
||||
|
||||
if(string.length > maxSplitLength) {
|
||||
let tempStrings = []
|
||||
|
||||
for (let i = maxSplitLength; i < string.length; i = i + maxSplitLength) {
|
||||
let nearestIndex = indices.length > 0 ? indices.reduce(function(prev, curr) {
|
||||
return (Math.abs(curr - i) < Math.abs(prev - i) ? curr : prev);
|
||||
}) : i
|
||||
|
||||
tempStrings.push(string.substring(lastIndex,nearestIndex))
|
||||
|
||||
lastIndex = indices.length > 0 ? nearestIndex + 1 : nearestIndex
|
||||
}
|
||||
|
||||
tempStrings.push(string.substring(lastIndex,input.length))
|
||||
|
||||
returnSplitStrings.push(...tempStrings)
|
||||
|
||||
} else {
|
||||
returnSplitStrings.push(string)
|
||||
}
|
||||
})
|
||||
|
||||
return returnSplitStrings
|
||||
}
|
||||
Reference in New Issue
Block a user