Brings in the full backend CRM stack on top of the consolidated
migration (60abe8c).
Services (CRM)
- quoteService — full lifecycle (draft → sent → accepted → converted
to event/invoice), Skonto + Storno + reissue paths
- invoiceService — spawnInstallmentInvoices, updateInstallmentPlan,
monthly-billing accumulator, payment-check tokens, dunning ladder
- contractService — block-composable contract editor, in-browser
signature flow, wet-PDF upload path, integrity check, audit trail
- customerHoursService — per-entry locking, billing integration
- dealsService — cross-document lineage (deal_uuid)
- taxReportService — quarterly aggregates + CSV/PDF export
- eventReminderService — pre-event customer reminder cron pass
- _renderContext — shared issuer/recipient blocks across PDF types
- pdfService extensions — custom-font registration, font picker
Routes (admin + public)
- adminQuotes, adminInvoices, adminContracts, adminCalendar,
adminDeals, adminTaxReport, adminDev, adminBusinessProfile
- publicQuotes (accept/decline), publicContracts (sign),
publicPaymentCheck
- Extensions on adminEvents, adminCustomers, adminSettings,
adminEmail, adminFeatureFlags, adminThumbnails, adminPhotos,
adminCategories, adminUsers, adminArchives, adminDashboard
- server.js wires the new mounts (kept upstream's noStoreCache on
customer routes per 3-way merge)
Utilities
- schemaCache (cached hasColumn lookups across services)
- documentSequences (atomic gap-free numbering — §14 UStG)
- safePath (path-containment guards at fs stream boundaries)
- clientIp (sanctioned XFF reader for audit logs)
- publicTokenGuards (pre-multer token validation + attempt counters)
- numericHelpers (ensureInt / ensureNumber consolidation)
- dateFormatter (formatShortDate + dateInputLang)
- dbCompat extensions, iban + pdfFilename helpers, resolveLogoFile
Infrastructure
- Bundled PDF fonts (Comic-Neue / IBM-Plex-Sans / Inter / Jost /
Montserrat / Noto-Sans / Playfair-Display / Poppins)
- Backend package.json + lock updates (pdfkit, signature_pad,
qrcode, et al.)
- Sample storage layout under storage/business-docs/quote/
Tests
- 14 new test files covering quote/invoice/contract lifecycle,
installment plan reshape, line-item hierarchy, customer hours,
payment check, tax report PDF, IBAN parsing, filename sanitiser
123 lines
3.8 KiB
JavaScript
123 lines
3.8 KiB
JavaScript
const { db } = require('../database/db');
|
|
|
|
// Default date format settings
|
|
const DEFAULT_FORMAT = {
|
|
format: 'DD/MM/YYYY',
|
|
locale: 'en-GB'
|
|
};
|
|
|
|
// Format date based on system settings
|
|
async function formatDate(date, language = 'en') {
|
|
try {
|
|
// Get date format setting from database
|
|
const setting = await db('app_settings').where('setting_key', 'general_date_format').first();
|
|
let dateConfig = DEFAULT_FORMAT;
|
|
|
|
if (setting && setting.setting_value) {
|
|
// Handle both string and object values
|
|
if (typeof setting.setting_value === 'string') {
|
|
try {
|
|
dateConfig = JSON.parse(setting.setting_value);
|
|
} catch (e) {
|
|
console.warn('Failed to parse date format setting:', e.message);
|
|
dateConfig = DEFAULT_FORMAT;
|
|
}
|
|
} else {
|
|
dateConfig = setting.setting_value;
|
|
}
|
|
}
|
|
|
|
// Ensure proper date parsing
|
|
let dateObj;
|
|
if (date instanceof Date) {
|
|
dateObj = date;
|
|
} else if (typeof date === 'string') {
|
|
// For date strings like "2025-07-16", parse as local date to avoid timezone issues
|
|
if (date.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
|
// Parse YYYY-MM-DD format as local date
|
|
const [year, month, day] = date.split('-').map(num => parseInt(num, 10));
|
|
dateObj = new Date(year, month - 1, day);
|
|
} else {
|
|
dateObj = new Date(date);
|
|
}
|
|
} else {
|
|
dateObj = new Date(date);
|
|
}
|
|
|
|
// Check if date is valid
|
|
if (isNaN(dateObj.getTime())) {
|
|
console.error('Invalid date provided to formatDate:', date);
|
|
throw new Error('Invalid date');
|
|
}
|
|
|
|
// Use appropriate locale based on language
|
|
let locale = dateConfig.locale || 'en-GB';
|
|
if (language === 'de') {
|
|
locale = 'de-DE';
|
|
} else if (language === 'pt') {
|
|
locale = 'pt-BR';
|
|
} else if (language === 'en' && dateConfig.format === 'MM/DD/YYYY') {
|
|
locale = 'en-US';
|
|
}
|
|
|
|
// Format based on the configured format
|
|
switch (dateConfig.format) {
|
|
case 'MM/DD/YYYY':
|
|
return dateObj.toLocaleDateString(locale, {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
case 'DD/MM/YYYY':
|
|
return dateObj.toLocaleDateString(locale, {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
case 'YYYY-MM-DD':
|
|
return dateObj.toISOString().split('T')[0];
|
|
case 'DD.MM.YYYY':
|
|
return dateObj.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
default:
|
|
// Use long format as fallback
|
|
return dateObj.toLocaleDateString(locale, {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error formatting date:', error);
|
|
// Fallback to basic formatting
|
|
return date instanceof Date ? date.toLocaleDateString() : new Date(date).toLocaleDateString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync DD.MM.YYYY formatter used by quote / invoice / contract render
|
|
* contexts. Unlike `formatDate` above, this never consults app_settings
|
|
* — it's intended for fixed-format use inside templates already rendered
|
|
* for a specific document type. Three services used to ship a local
|
|
* copy each; this is the single source.
|
|
*
|
|
* - falsy input → empty string (template's {{#if ...}} block hides)
|
|
* - invalid date → the original value coerced to String (defensive
|
|
* passthrough; matches the prior behaviour of the three local copies)
|
|
*/
|
|
function formatShortDate(value) {
|
|
if (!value) return '';
|
|
const d = value instanceof Date ? value : new Date(value);
|
|
if (Number.isNaN(d.getTime())) return String(value);
|
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
return `${dd}.${mm}.${d.getFullYear()}`;
|
|
}
|
|
|
|
module.exports = {
|
|
formatDate,
|
|
formatShortDate,
|
|
}; |