Changes
This commit is contained in:
103
src/utils/diff.ts
Normal file
103
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
src/utils/diffTranslations.ts
Normal file
165
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",
|
||||
}),
|
||||
},
|
||||
};
|
||||
69
src/utils/history.ts
Normal file
69
src/utils/history.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
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"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
32
src/utils/mailer.ts
Normal file
32
src/utils/mailer.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import nodemailer from "nodemailer"
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT) || 587,
|
||||
secure: process.env.SMTP_SSL === "true", // true für 465, false für andere Ports
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
})
|
||||
|
||||
export async function sendMail(
|
||||
to: string,
|
||||
subject: string,
|
||||
html: string
|
||||
): Promise<{ success: boolean; info?: any; error?: any }> {
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: process.env.MAIL_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
src/utils/password.ts
Normal file
15
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)
|
||||
}
|
||||
Reference in New Issue
Block a user