Add Azure Blob Storage archive & email safety net

- Add AzureBlobStorageService, DocumentArchiveSyncService, and related config for secondary PDF archiving of invoices/reminders
- Add SQL procs and schema changes for archive backfill
- Update invoice/reminder services to upload PDFs to blob storage
- Add telemetry counters and unit tests for blob storage/archive logic
- Add Fuchs:Email:OverrideRecipient config and enforce dev/test email redirect in ProcessWebComService, with tests
- Improve JS date parsing (German formats), stricter JSON date detection
- Increase widget SQL timeouts, update dependencies, docs, and project files
This commit is contained in:
Stefan
2026-07-03 10:15:04 +02:00
parent 3035ef1719
commit c3395bc83c
47 changed files with 1723 additions and 101 deletions
+17 -4
View File
@@ -168,21 +168,34 @@ function ne(inp, alt) {
return (inp || '') === '' ? (alt || '') : inp;
}
function pad(i, n) { return (i || '').toString().padStart(n, '0').substr(-1 * n); }
function twoDigitYear(y) { var n = parseInt(y, 10); return n + (n < 70 ? 2000 : 1900); }
/* Parses dot-separated German short dates (dd.MM.yyyy / dd.MM.yy, optionally with a time part).
Returns null if the string does not match, so callers can fall back to other parsing. */
function parseGermanDate(si) {
var g = si.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2}|\d{4})(?:[\sT](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
if (g === null) { return null; }
var yr = g[3].length === 2 ? twoDigitYear(g[3]) : parseInt(g[3], 10);
return new Date(yr, parseInt(g[2], 10) - 1, parseInt(g[1], 10), parseInt(g[4] || '0', 10), parseInt(g[5] || '0', 10), parseInt(g[6] || '0', 10));
}
function parseISO(s) {
let si = s || '';
if (si === '') { return null };
if (/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/.test(si) === true) {
return new Date(si);
} else {
var b = s.split(/\D/);
return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]);
var gd = parseGermanDate(si);
if (gd !== null) { return gd; }
var b = si.split(/\D/);
return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0);
}
}
function parseISOLocal(s) {
let si = s || '';
if (si === '') { return null };
var b = s.split(/\D/);
return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]);
var gd = parseGermanDate(si);
if (gd !== null) { return gd; }
var b = si.split(/\D/);
return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0);
}
function fnum(i, style) {
/* { style: 'decimal/currency/percent', currency: 'USD/EUR', currencyDisplay: 'symbol/code/name', minimumIntegerDigits: 1, minimumFractionDigits: 2, maximumFractionDigits: 3, useGrouping: true } */