Logbuch Überarbeitung
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 29s
Build and Push Docker Images / build-frontend (push) Successful in 16s

This commit is contained in:
2026-02-15 20:43:01 +01:00
parent cb4917c536
commit db4e9612a0
4 changed files with 259 additions and 11 deletions

View File

@@ -12,6 +12,8 @@ import {
import { resourceConfig } from "../../utils/resource.config";
import { useNextNumberRangeNumber } from "../../utils/functions";
import { insertHistoryItem } from "../../utils/history";
import { diffObjects } from "../../utils/diff";
import { recalculateServicePricesForTenant } from "../../modules/service-price-recalculation.service";
// -------------------------------------------------------------
@@ -31,6 +33,42 @@ function buildSearchCondition(columns: any[], search: string) {
return or(...conditions)
}
function formatDiffValue(value: any): string {
if (value === null || value === undefined) return "-"
if (typeof value === "boolean") return value ? "Ja" : "Nein"
if (typeof value === "object") {
try {
return JSON.stringify(value)
} catch {
return "[Objekt]"
}
}
return String(value)
}
const TECHNICAL_HISTORY_KEYS = new Set([
"id",
"tenant",
"tenant_id",
"createdAt",
"created_at",
"createdBy",
"created_by",
"updatedAt",
"updated_at",
"updatedBy",
"updated_by",
"archived",
])
function getUserVisibleChanges(oldRecord: Record<string, any>, updated: Record<string, any>) {
return diffObjects(oldRecord, updated).filter((c) => !TECHNICAL_HISTORY_KEYS.has(c.key))
}
function buildFieldUpdateHistoryText(resource: string, label: string, oldValue: any, newValue: any) {
return `${resource}: ${label} geändert von "${formatDiffValue(oldValue)}" zu "${formatDiffValue(newValue)}"`
}
export default async function resourceRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
@@ -349,6 +387,23 @@ export default async function resourceRoutes(server: FastifyInstance) {
await recalculateServicePricesForTenant(server, req.user.tenant_id, req.user?.user_id || null);
}
if (created) {
try {
await insertHistoryItem(server, {
tenant_id: req.user.tenant_id,
created_by: req.user?.user_id || null,
entity: resource,
entityId: created.id,
action: "created",
oldVal: null,
newVal: created,
text: `Neuer Eintrag in ${resource} erstellt`,
})
} catch (historyError) {
server.log.warn({ err: historyError, resource }, "Failed to write create history entry")
}
}
return created;
} catch (error) {
console.error(error);
@@ -369,6 +424,12 @@ export default async function resourceRoutes(server: FastifyInstance) {
const table = resourceConfig[resource].table
const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; }
const [oldRecord] = await server.db
.select()
.from(table)
.where(and(eq(table.id, id), eq(table.tenant, tenantId)))
.limit(1)
let data = { ...body, updated_at: new Date().toISOString(), updated_by: userId }
//@ts-ignore
delete data.updatedBy; delete data.updatedAt;
@@ -385,6 +446,39 @@ export default async function resourceRoutes(server: FastifyInstance) {
await recalculateServicePricesForTenant(server, tenantId, userId);
}
if (updated) {
try {
const changes = oldRecord ? getUserVisibleChanges(oldRecord, updated) : []
if (!changes.length) {
await insertHistoryItem(server, {
tenant_id: tenantId,
created_by: userId,
entity: resource,
entityId: updated.id,
action: "updated",
oldVal: oldRecord || null,
newVal: updated,
text: `Eintrag in ${resource} geändert`,
})
} else {
for (const change of changes) {
await insertHistoryItem(server, {
tenant_id: tenantId,
created_by: userId,
entity: resource,
entityId: updated.id,
action: "updated",
oldVal: change.oldValue,
newVal: change.newValue,
text: buildFieldUpdateHistoryText(resource, change.label, change.oldValue, change.newValue),
})
}
}
} catch (historyError) {
server.log.warn({ err: historyError, resource, id }, "Failed to write update history entry")
}
}
return updated
} catch (err) {
console.error(err)

View File

@@ -1,5 +1,5 @@
import {diffTranslations} from "./diffTranslations";
import {diffTranslations, getDiffLabel} from "./diffTranslations";
export type DiffChange = {
key: string;
@@ -43,8 +43,6 @@ export function diffObjects(
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) === "[]") &&
@@ -72,12 +70,11 @@ export function diffObjects(
if (type === "unchanged") continue;
const translation = diffTranslations[key];
let label = key;
let label = getDiffLabel(key);
let resolvedOld = oldVal;
let resolvedNew = newVal;
if (translation) {
label = translation.label;
if (translation.resolve) {
const { oldVal: resOld, newVal: resNew } = translation.resolve(
oldVal,

View File

@@ -6,6 +6,149 @@ type ValueResolver = (
ctx?: Record<string, any>
) => { oldVal: any; newVal: any };
const TOKEN_TRANSLATIONS: Record<string, string> = {
account: "Konto",
active: "Aktiv",
address: "Adresse",
amount: "Betrag",
archived: "Archiviert",
article: "Artikel",
bank: "Bank",
barcode: "Barcode",
birthday: "Geburtstag",
category: "Kategorie",
city: "Ort",
color: "Farbe",
comment: "Kommentar",
company: "Firma",
contact: "Kontakt",
contract: "Vertrag",
cost: "Kosten",
country: "Land",
created: "Erstellt",
customer: "Kunde",
date: "Datum",
default: "Standard",
deleted: "Gelöscht",
delivery: "Lieferung",
description: "Beschreibung",
document: "Dokument",
driver: "Fahrer",
due: "Fällig",
duration: "Dauer",
email: "E-Mail",
employee: "Mitarbeiter",
enabled: "Aktiviert",
end: "Ende",
event: "Ereignis",
file: "Datei",
first: "Vorname",
fixed: "Festgeschrieben",
group: "Gruppe",
hour: "Stunde",
iban: "IBAN",
id: "ID",
incoming: "Eingang",
invoice: "Rechnung",
item: "Eintrag",
language: "Sprache",
last: "Nachname",
license: "Kennzeichen",
link: "Link",
list: "Liste",
location: "Standort",
manufacturer: "Hersteller",
markup: "Verkaufsaufschlag",
message: "Nachricht",
mobile: "Mobil",
name: "Name",
note: "Notiz",
notes: "Notizen",
number: "Nummer",
order: "Bestellung",
own: "Eigen",
payment: "Zahlung",
phone: "Telefon",
plant: "Objekt",
postal: "Post",
price: "Preis",
percentage: "%",
product: "Produkt",
profile: "Profil",
project: "Projekt",
purchase: "Kauf",
quantity: "Menge",
rate: "Satz",
reference: "Referenz",
requisition: "Anfrage",
resource: "Ressource",
role: "Rolle",
serial: "Serien",
service: "Leistung",
selling: "Verkauf",
sellign: "Verkauf",
space: "Lagerplatz",
start: "Start",
statement: "Buchung",
status: "Status",
street: "Straße",
surcharge: "Aufschlag",
tax: "Steuer",
tel: "Telefon",
tenant: "Mandant",
time: "Zeit",
title: "Titel",
total: "Gesamt",
type: "Typ",
unit: "Einheit",
updated: "Aktualisiert",
user: "Benutzer",
ustid: "USt-ID",
value: "Wert",
vendor: "Lieferant",
vehicle: "Fahrzeug",
weekly: "Wöchentlich",
working: "Arbeits",
zip: "Postleitzahl",
composed: "Zusammensetzung",
material: "Material",
worker: "Arbeit",
};
function tokenizeKey(key: string): string[] {
return key
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/[^a-zA-Z0-9]+/g, "_")
.split("_")
.filter(Boolean)
.map((p) => p.toLowerCase());
}
function capitalize(word: string) {
if (!word) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
}
function fallbackLabelFromKey(key: string): string {
const parts = tokenizeKey(key);
if (!parts.length) return key;
if (parts.length > 1 && parts[parts.length - 1] === "id") {
const base = parts.slice(0, -1).map((p) => TOKEN_TRANSLATIONS[p] || capitalize(p)).join(" ");
return `${base} ID`.trim();
}
return parts
.map((p) => TOKEN_TRANSLATIONS[p] || capitalize(p))
.join(" ")
.replace(/\s+/g, " ")
.trim();
}
export function getDiffLabel(key: string): string {
return diffTranslations[key]?.label || fallbackLabelFromKey(key);
}
export const diffTranslations: Record<
string,
{ label: string; resolve?: ValueResolver }
@@ -44,7 +187,7 @@ export const diffTranslations: Record<
}),
},
resources: {
label: "Resourcen",
label: "Ressourcen",
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(", ") : "-",
@@ -86,6 +229,11 @@ export const diffTranslations: Record<
approved: { label: "Genehmigt" },
manufacturer: { label: "Hersteller" },
purchasePrice: { label: "Kaufpreis" },
markupPercentage: { label: "Verkaufsaufschlag in %" },
markup_percentage: { label: "Verkaufsaufschlag in %" },
sellingPrice: { label: "Verkaufspreis" },
selling_price: { label: "Verkaufspreis" },
sellingPriceComposed: { label: "Verkaufspreis Zusammensetzung" },
purchaseDate: { label: "Kaufdatum" },
serialNumber: { label: "Seriennummer" },
usePlanning: { label: "In Plantafel verwenden" },
@@ -108,6 +256,7 @@ export const diffTranslations: Record<
description: { label: "Beschreibung" },
categorie: { label: "Kategorie" },
category: { label: "Kategorie" },
profile: {
label: "Mitarbeiter",

View File

@@ -17,6 +17,7 @@ export async function insertHistoryItem(
const textMap = {
created: `Neuer Eintrag in ${params.entity} erstellt`,
updated: `Eintrag in ${params.entity} geändert`,
unchanged: `Eintrag in ${params.entity} unverändert`,
archived: `Eintrag in ${params.entity} archiviert`,
deleted: `Eintrag in ${params.entity} gelöscht`
}
@@ -45,7 +46,9 @@ export async function insertHistoryItem(
trackingtrips: "trackingtrip",
createddocuments: "createddocument",
inventoryitemgroups: "inventoryitemgroup",
bankstatements: "bankstatement"
bankstatements: "bankstatement",
incominginvoices: "incomingInvoice",
files: "file",
}
const fkColumn = columnMap[params.entity]
@@ -54,14 +57,19 @@ export async function insertHistoryItem(
return
}
const stringifyHistoryValue = (value: any) => {
if (value === undefined || value === null) return null
return typeof value === "string" ? value : JSON.stringify(value)
}
const entry = {
tenant: params.tenant_id,
created_by: params.created_by,
createdBy: 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
oldVal: stringifyHistoryValue(params.oldVal),
newVal: stringifyHistoryValue(params.newVal)
}
await server.db.insert(historyitems).values(entry as any)