fix(agrarmonitor): gebuchte Belege wieder erkennen, Titel per Template
Build and Push Multi-Platform Images / build-and-push (push) Successful in 18s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 18s
- Connector auf 9bda65e: Buchungsstatus aus #bookedStatus lesen. AM zeigt hinter dem Status jetzt „Zahlung erfassen“, dadurch war buchungsDatum immer leer und das Polling hat keinen Beleg als verbucht markiert. - Polling: Schritt-Logging pro Dokument über AGRARMONITOR_POLLING_DEBUG, Abschlusszeile zählt „noch nicht gebucht“ mit. - Upload-Check: Titel über das Titel-Template des Dokumenttyps, Fallback auf ERG/EGU <Belegnr>. Template-Auflösung als resolveTitelTemplate() ausgelagert und im Processor wiederverwendet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df50470e53
commit
bc2d1cd4b7
8 files changed
+208
-65
No files matched your search
@@ -68,6 +68,7 @@ AGRARMONITOR_COOKIE_PATH=./data/agrarmonitor-cookies.json
|
||||
AGRARMONITOR_ENCRYPTION_KEY= # optional, 16+ Zeichen für Cookie-Verschlüsselung
|
||||
AGRARMONITOR_POLLING_CRON=0 */30 * * * * # Polling-Intervall (Standard: alle 30 Minuten); leer lassen zum Deaktivieren
|
||||
AGRARMONITOR_UPLOAD_CHECK_CRON=0 * * * * * # Upload-Check-Intervall (Standard: einmal pro Minute); leer lassen zum Deaktivieren
|
||||
AGRARMONITOR_POLLING_DEBUG= # true = jeden Schritt des Polling-Laufs pro Dokument loggen
|
||||
|
||||
# --- Täglicher Digest ---
|
||||
# Basis-URL der App für klickbare Links in Digest-E-Mails (z.B. https://paperless.example.com)
|
||||
|
||||
@@ -51,6 +51,7 @@ services:
|
||||
- AGRARMONITOR_ENCRYPTION_KEY=${AGRARMONITOR_ENCRYPTION_KEY:-}
|
||||
- AGRARMONITOR_POLLING_CRON=${AGRARMONITOR_POLLING_CRON:-}
|
||||
- AGRARMONITOR_UPLOAD_CHECK_CRON=${AGRARMONITOR_UPLOAD_CHECK_CRON:-}
|
||||
- AGRARMONITOR_POLLING_DEBUG=${AGRARMONITOR_POLLING_DEBUG:-}
|
||||
- APP_URL=${APP_URL:-}
|
||||
- DAILY_DIGEST_CRON=${DAILY_DIGEST_CRON:-}
|
||||
volumes:
|
||||
|
||||
Generated
+1
-1
@@ -5064,7 +5064,7 @@
|
||||
},
|
||||
"node_modules/agrarmonitor-connector": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git#e6ce1d254bf21522b73a9323bb8f8cd9fadd7eef",
|
||||
"resolved": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git#9bda65eec0ec45c98ecce506fe68e36991c0ae61",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
|
||||
@@ -8,6 +8,8 @@ import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { Setting } from '../database/entities/setting.entity';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
import { DocumentType } from '../database/entities/document-type.entity';
|
||||
import { resolveTitelTemplate } from '../paperless/titel-template.util';
|
||||
|
||||
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
||||
const EINGANGSDATUM_FIELD_ID = 9;
|
||||
@@ -47,6 +49,10 @@ export interface SyncCorrespondentsResult {
|
||||
export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
private readonly logger = new Logger(AgrarmonitorPollingService.name);
|
||||
private pollingRunning = false;
|
||||
/** Schritt-Logging für runPolling(), aktivierbar über AGRARMONITOR_POLLING_DEBUG=true */
|
||||
private readonly debug = ['1', 'true', 'yes'].includes(
|
||||
(process.env['AGRARMONITOR_POLLING_DEBUG'] ?? '').toLowerCase(),
|
||||
);
|
||||
private uploadCheckRunning = false;
|
||||
|
||||
constructor(
|
||||
@@ -57,6 +63,8 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
||||
@InjectRepository(CorrespondentSetting)
|
||||
private readonly corrSettingRepo: Repository<CorrespondentSetting>,
|
||||
@InjectRepository(DocumentType)
|
||||
private readonly docTypeRepo: Repository<DocumentType>,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
@@ -210,6 +218,10 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
const tagFertigId = parseInt(tagFertigSetting?.Wert ?? '4', 10);
|
||||
const tagVerbuchtId = parseInt(tagVerbuchtSetting?.Wert ?? '9', 10);
|
||||
|
||||
this.trace(
|
||||
`Konfiguration: Tag fertig=${tagFertigId}, Tag verbucht=${tagVerbuchtId}`,
|
||||
);
|
||||
|
||||
if (isNaN(tagFertigId) || isNaN(tagVerbuchtId)) {
|
||||
const msg = 'Tag-IDs ungültig (keine Zahlen)';
|
||||
this.logger.error(msg);
|
||||
@@ -236,9 +248,13 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
return { ...result, errors: [msg] };
|
||||
}
|
||||
|
||||
for (const customer of customers.filter(
|
||||
const aktiveLieferanten = customers.filter(
|
||||
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
||||
)) {
|
||||
);
|
||||
this.trace(
|
||||
`${customers.length} Kunden geladen, davon ${aktiveLieferanten.length} aktive Lieferanten — starte Korrespondenten-Sync`,
|
||||
);
|
||||
for (const customer of aktiveLieferanten) {
|
||||
try {
|
||||
await this.getOrCreateCorrespondent(customer, Number(customer.id));
|
||||
} catch (err: unknown) {
|
||||
@@ -261,9 +277,14 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
this.logger.log(`${docs.length} Dokumente fertig in Agrarmonitor`);
|
||||
this.trace(
|
||||
`Paperless meldet ${docsResponse?.count ?? 0} Dokumente mit Tag ${tagFertigId}`,
|
||||
);
|
||||
let nichtGebucht = 0;
|
||||
|
||||
for (const doc of docs) {
|
||||
result.processed++;
|
||||
const nr = `[${result.processed}/${docs.length}] Dokument ${doc.id as number}`;
|
||||
|
||||
const interneBelegnummer =
|
||||
(((doc.custom_fields as any[]) ?? []).find(
|
||||
@@ -279,6 +300,10 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.trace(
|
||||
`${nr} „${doc.title as string}“: interne Belegnummer ${interneBelegnummer}, Tags [${((doc.tags as number[]) ?? []).join(', ')}] — starte Livesearch`,
|
||||
);
|
||||
|
||||
let amResults: Awaited<
|
||||
ReturnType<typeof amClient.eingangsrechnungenLivesearch>
|
||||
>;
|
||||
@@ -295,14 +320,17 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
break;
|
||||
}
|
||||
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
||||
this.logger.error(
|
||||
`${msg}: ${fehlerText(err)}`,
|
||||
);
|
||||
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||
result.errors.push(msg);
|
||||
await this.delay(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.trace(
|
||||
`${nr}: Livesearch lieferte ${amResults.length} Treffer` +
|
||||
amResults.map((r) => `\n ${this.describeAmDoc(r)}`).join(''),
|
||||
);
|
||||
|
||||
if (amResults.length === 0) {
|
||||
this.logger.log(
|
||||
`${interneBelegnummer} nicht in Agrarmonitor gefunden`,
|
||||
@@ -323,6 +351,9 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
const amDoc = amResults[0];
|
||||
|
||||
if (!amDoc.interneBelegNummer && interneBelegnummer) {
|
||||
this.trace(
|
||||
`${nr}: Lieferscheinnummer in AM leer — setze ${interneBelegnummer}`,
|
||||
);
|
||||
try {
|
||||
await amClient.setLieferscheinNummer(
|
||||
amDoc.eingangId,
|
||||
@@ -339,6 +370,9 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
const eingangsdatumField = ((doc.custom_fields as any[]) ?? []).find(
|
||||
(cf: any) => cf.field === EINGANGSDATUM_FIELD_ID,
|
||||
);
|
||||
this.trace(
|
||||
`${nr}: Eingangsdatum in AM leer — Paperless-Feld ${EINGANGSDATUM_FIELD_ID} = ${JSON.stringify(eingangsdatumField?.value ?? null)}`,
|
||||
);
|
||||
if (eingangsdatumField?.value) {
|
||||
const eingangsdatum = new Date(eingangsdatumField.value as string);
|
||||
if (!isNaN(eingangsdatum.getTime())) {
|
||||
@@ -350,7 +384,17 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
if (!amDoc.buchungsDatum) {
|
||||
nichtGebucht++;
|
||||
this.trace(
|
||||
`${nr}: kein Buchungsdatum in AM — noch nicht gebucht, keine Änderung`,
|
||||
);
|
||||
}
|
||||
|
||||
if (amDoc.buchungsDatum) {
|
||||
this.trace(
|
||||
`${nr}: gebucht am ${this.fmtDate(amDoc.buchungsDatum)} — aktualisiere Paperless`,
|
||||
);
|
||||
try {
|
||||
let correspondentId: number | undefined;
|
||||
const customer = customers.find(
|
||||
@@ -384,6 +428,9 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
updateData.correspondent = correspondentId;
|
||||
if (ownerId !== undefined) updateData.owner = ownerId;
|
||||
|
||||
this.trace(
|
||||
`${nr}: Kunde ${amDoc.kundenId} → Korrespondent ${correspondentId ?? '–'}, Betrieb ${amDoc.betriebId} → Owner ${ownerId ?? '–'}, Update ${JSON.stringify(updateData)}`,
|
||||
);
|
||||
await this.paperlessService.updateDocument(
|
||||
doc.id as number,
|
||||
updateData,
|
||||
@@ -392,9 +439,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
result.updated++;
|
||||
} catch (err: unknown) {
|
||||
const msg = `${interneBelegnummer}: Update-Fehler`;
|
||||
this.logger.error(
|
||||
`${msg}: ${fehlerText(err)}`,
|
||||
);
|
||||
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||
result.errors.push(msg);
|
||||
}
|
||||
}
|
||||
@@ -404,7 +449,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
|
||||
this.logger.log(
|
||||
`Polling abgeschlossen: ${result.processed} verarbeitet, ${result.updated} aktualisiert, ` +
|
||||
`${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
||||
`${nichtGebucht} noch nicht gebucht, ${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
||||
);
|
||||
} finally {
|
||||
this.pollingRunning = false;
|
||||
@@ -525,9 +570,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
break;
|
||||
}
|
||||
const msg = `${interneBelegnummer}: Vorhanden-Check fehlgeschlagen`;
|
||||
this.logger.error(
|
||||
`${msg}: ${fehlerText(err)}`,
|
||||
);
|
||||
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||
result.errors.push(msg);
|
||||
await this.delay(500);
|
||||
continue;
|
||||
@@ -552,9 +595,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
}
|
||||
// Bei Fehler vorsichtig: nicht verschieben
|
||||
const msg = `${interneBelegnummer}: Dateieingang-Check fehlgeschlagen`;
|
||||
this.logger.error(
|
||||
`${msg}: ${fehlerText(err)}`,
|
||||
);
|
||||
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||
result.errors.push(msg);
|
||||
await this.delay(500);
|
||||
continue;
|
||||
@@ -645,9 +686,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
break;
|
||||
}
|
||||
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
||||
this.logger.error(
|
||||
`${msg}: ${fehlerText(err)}`,
|
||||
);
|
||||
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||
result.errors.push(msg);
|
||||
await this.delay(500);
|
||||
continue;
|
||||
@@ -718,10 +757,23 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
|
||||
// Titel über das Titel-Template des Dokumenttyps bilden;
|
||||
// ohne (auflösbares) Template bleibt der bisherige Standardtitel.
|
||||
const documentTypeId = amDoc.dokumentTyp === 0 ? 1 : 2;
|
||||
const docTypeConfig = await this.docTypeRepo.findOneBy({
|
||||
DocumentTypeId: documentTypeId,
|
||||
});
|
||||
const title =
|
||||
resolveTitelTemplate(
|
||||
docTypeConfig?.TitelTemplate,
|
||||
existingFields,
|
||||
amDoc.belegDatum ?? new Date(doc.created as string),
|
||||
) ??
|
||||
(amDoc.dokumentTyp === 0 ? 'ERG ' : 'EGU ') + amDoc.belegNummer;
|
||||
|
||||
const updateData: Record<string, any> = {
|
||||
title:
|
||||
(amDoc.dokumentTyp === 0 ? 'ERG ' : 'EGU ') + amDoc.belegNummer,
|
||||
document_type: amDoc.dokumentTyp === 0 ? 1 : 2,
|
||||
title,
|
||||
document_type: documentTypeId,
|
||||
tags: newTags,
|
||||
custom_fields: existingFields,
|
||||
};
|
||||
@@ -742,9 +794,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
result.updated++;
|
||||
} catch (err: unknown) {
|
||||
const msg = `${interneBelegnummer}: Update-Fehler`;
|
||||
this.logger.error(
|
||||
`${msg}: ${fehlerText(err)}`,
|
||||
);
|
||||
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||
result.errors.push(msg);
|
||||
}
|
||||
|
||||
@@ -1018,6 +1068,34 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
return `${name} (${nummer})`;
|
||||
}
|
||||
|
||||
private trace(message: string): void {
|
||||
if (this.debug) this.logger.log(`[Debug] ${message}`);
|
||||
}
|
||||
|
||||
private fmtDate(d: Date | null | undefined): string {
|
||||
if (!d) return '–';
|
||||
return isNaN(d.getTime()) ? 'ungültig' : d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
private describeAmDoc(r: {
|
||||
eingangId: number;
|
||||
belegNummer: string;
|
||||
interneBelegNummer: string;
|
||||
belegDatum: Date | null;
|
||||
eingangsDatum: Date | null;
|
||||
buchungsDatum: Date | null;
|
||||
kundenId: number;
|
||||
betriebId: number;
|
||||
dokumentTyp: number;
|
||||
}): string {
|
||||
return (
|
||||
`eingangId=${r.eingangId} beleg=${r.belegNummer || '–'} intern=${r.interneBelegNummer || '–'} ` +
|
||||
`typ=${r.dokumentTyp} belegDatum=${this.fmtDate(r.belegDatum)} ` +
|
||||
`eingangsDatum=${this.fmtDate(r.eingangsDatum)} buchungsDatum=${this.fmtDate(r.buchungsDatum)} ` +
|
||||
`kunde=${r.kundenId} betrieb=${r.betriebId}`
|
||||
);
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -8,10 +8,16 @@ import { PaperlessModule } from '../paperless/paperless.module';
|
||||
import { Setting } from '../database/entities/setting.entity';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
import { DocumentType } from '../database/entities/document-type.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Setting, Client, CorrespondentSetting]),
|
||||
TypeOrmModule.forFeature([
|
||||
Setting,
|
||||
Client,
|
||||
CorrespondentSetting,
|
||||
DocumentType,
|
||||
]),
|
||||
PaperlessModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
|
||||
import { DocumentField } from '../database/entities/document-field.entity';
|
||||
import { DocumentType } from '../database/entities/document-type.entity';
|
||||
import { PaperlessService } from './paperless.service';
|
||||
import { resolveTitelTemplate } from './titel-template.util';
|
||||
import { PostprocessingService } from '../postprocessing/postprocessing.service';
|
||||
|
||||
const PAPERLESSMANAGER_TAG_ID = 16; // Tag "paperlessmanager"
|
||||
@@ -220,45 +221,12 @@ export class PaperlessProcessorService {
|
||||
};
|
||||
|
||||
if (docTypeConfig.TitelTemplate) {
|
||||
let title = docTypeConfig.TitelTemplate;
|
||||
|
||||
// Prüfen ob alle im Template referenzierten Custom Fields ausgefüllt sind
|
||||
const placeholderRegex = /\{\{CUSTOM\[(\d+)\]\}\}/g;
|
||||
let allFilled = true;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = placeholderRegex.exec(title)) !== null) {
|
||||
const fieldId = parseInt(match[1], 10);
|
||||
const cf = newCustomFields.find((f) => f.field === fieldId);
|
||||
if (!cf || cf.value == null || cf.value === '') {
|
||||
allFilled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allFilled) {
|
||||
for (const cf of newCustomFields) {
|
||||
const placeholder = `{{CUSTOM[${cf.field}]}}`;
|
||||
if (title.includes(placeholder)) {
|
||||
title = title.replaceAll(
|
||||
placeholder,
|
||||
cf.value != null ? String(cf.value) : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (title.includes('{{DATE}}')) {
|
||||
const created = doc.created ? new Date(doc.created) : new Date();
|
||||
const dateStr = `${String(created.getDate()).padStart(2, '0')}.${String(created.getMonth() + 1).padStart(2, '0')}.${created.getFullYear()}`;
|
||||
title = title.replaceAll('{{DATE}}', dateStr);
|
||||
}
|
||||
|
||||
if (title.includes('{{ZEITSTEMPEL}}')) {
|
||||
const now = new Date();
|
||||
const ts = `${String(now.getDate()).padStart(2, '0')}.${String(now.getMonth() + 1).padStart(2, '0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
title = title.replaceAll('{{ZEITSTEMPEL}}', ts);
|
||||
}
|
||||
|
||||
const title = resolveTitelTemplate(
|
||||
docTypeConfig.TitelTemplate,
|
||||
newCustomFields,
|
||||
doc.created ? new Date(doc.created) : new Date(),
|
||||
);
|
||||
if (title !== null) {
|
||||
updatePayload.title = title;
|
||||
} else {
|
||||
this.logger.log(
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { resolveTitelTemplate } from './titel-template.util';
|
||||
|
||||
describe('resolveTitelTemplate', () => {
|
||||
const created = new Date(2026, 9, 5); // 05.10.2026
|
||||
|
||||
it('ersetzt Custom-Field-Platzhalter und {{DATE}}', () => {
|
||||
expect(
|
||||
resolveTitelTemplate(
|
||||
'ERG {{CUSTOM[3]}} vom {{DATE}}',
|
||||
[{ field: 3, value: 'RE-42' }],
|
||||
created,
|
||||
),
|
||||
).toBe('ERG RE-42 vom 05.10.2026');
|
||||
});
|
||||
|
||||
it('ersetzt mehrfach vorkommende Platzhalter', () => {
|
||||
expect(
|
||||
resolveTitelTemplate(
|
||||
'{{CUSTOM[3]}}/{{CUSTOM[3]}}',
|
||||
[{ field: 3, value: 'X' }],
|
||||
created,
|
||||
),
|
||||
).toBe('X/X');
|
||||
});
|
||||
|
||||
it('liefert null, wenn ein referenziertes Feld leer ist oder fehlt', () => {
|
||||
expect(
|
||||
resolveTitelTemplate('{{CUSTOM[3]}}', [{ field: 3, value: '' }], created),
|
||||
).toBeNull();
|
||||
expect(resolveTitelTemplate('{{CUSTOM[7]}}', [], created)).toBeNull();
|
||||
});
|
||||
|
||||
it('liefert null bei leerem Template', () => {
|
||||
expect(resolveTitelTemplate('', [], created)).toBeNull();
|
||||
expect(resolveTitelTemplate(null, [], created)).toBeNull();
|
||||
});
|
||||
|
||||
it('ersetzt {{ZEITSTEMPEL}} mit der übergebenen Uhrzeit', () => {
|
||||
expect(
|
||||
resolveTitelTemplate(
|
||||
'T {{ZEITSTEMPEL}}',
|
||||
[],
|
||||
created,
|
||||
new Date(2026, 0, 2, 3, 4, 5),
|
||||
),
|
||||
).toBe('T 02.01.2026 03:04:05');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
const formatDatum = (d: Date) =>
|
||||
`${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()}`;
|
||||
|
||||
/**
|
||||
* Löst ein Titel-Template eines Dokumenttyps auf.
|
||||
* Platzhalter: {{CUSTOM[<feldId>]}}, {{DATE}} (Belegdatum), {{ZEITSTEMPEL}} (jetzt).
|
||||
* Liefert null, wenn kein Template gesetzt ist oder ein referenziertes
|
||||
* Custom Field leer ist — der Aufrufer behält dann den bisherigen Titel.
|
||||
*/
|
||||
export function resolveTitelTemplate(
|
||||
template: string | null | undefined,
|
||||
customFields: Array<{ field: number; value: unknown }>,
|
||||
created: Date,
|
||||
now: Date = new Date(),
|
||||
): string | null {
|
||||
if (!template) return null;
|
||||
|
||||
const placeholderRegex = /\{\{CUSTOM\[(\d+)\]\}\}/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = placeholderRegex.exec(template)) !== null) {
|
||||
const fieldId = parseInt(match[1], 10);
|
||||
const cf = customFields.find((f) => f.field === fieldId);
|
||||
if (!cf || cf.value == null || cf.value === '') return null;
|
||||
}
|
||||
|
||||
let title = template;
|
||||
for (const cf of customFields) {
|
||||
title = title.replaceAll(
|
||||
`{{CUSTOM[${cf.field}]}}`,
|
||||
cf.value != null ? String(cf.value as string | number | boolean) : '',
|
||||
);
|
||||
}
|
||||
title = title.replaceAll('{{DATE}}', formatDatum(created));
|
||||
title = title.replaceAll(
|
||||
'{{ZEITSTEMPEL}}',
|
||||
`${formatDatum(now)} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
|
||||
);
|
||||
return title;
|
||||
}
|
||||
Reference in new issue
Block a user