CentralBackend/scripts/compareAboContractDocxVersions.js
seaznCode 427c12be3c feat: add language normalization and user settings updates
- Introduced language normalization utility functions to standardize language codes across the application.
- Updated ContractUploadController to resolve requested language from contract data and user settings.
- Enhanced authMiddleware to set preferred language in user object based on user settings.
- Added preferred_language column to user_settings table in the database.
- Implemented UserSettingsRepository to manage user settings, including preferred language updates.
- Updated DocumentTemplateService and AboContractService to support language-specific templates.
- Enhanced InvoiceService to select invoice templates based on normalized language codes.
- Added new script to compare versions of ABO contract documents.
- Refactored various services and repositories to utilize the new language normalization logic.
2026-06-07 21:13:41 +02:00

223 lines
6.1 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const yauzl = require('yauzl');
const ABO_TEMPLATE_DIR = path.join(__dirname, '..', 'templates', 'abo');
const COMPARISONS = [
{
label: 'DE',
previous: path.join(ABO_TEMPLATE_DIR, 'abo-contract-DE.docx'),
next: path.join(ABO_TEMPLATE_DIR, 'new', 'abo-contract-DE-NEW.docx'),
},
{
label: 'SL',
previous: path.join(ABO_TEMPLATE_DIR, 'abo-contract-SL.docx'),
next: path.join(ABO_TEMPLATE_DIR, 'new', 'abo-contract-SL-NEW.docx'),
},
];
function decodeXml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'");
}
function openZip(zipPath) {
return new Promise((resolve, reject) => {
yauzl.open(zipPath, { lazyEntries: true }, (error, zipFile) => {
if (error) {
reject(error);
return;
}
resolve(zipFile);
});
});
}
function readEntry(zipFile, entry) {
return new Promise((resolve, reject) => {
zipFile.openReadStream(entry, (error, stream) => {
if (error) {
reject(error);
return;
}
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
stream.on('error', reject);
});
});
}
async function readWordXmlEntries(zipPath) {
const zipFile = await openZip(zipPath);
const documents = [];
return new Promise((resolve, reject) => {
zipFile.readEntry();
zipFile.on('entry', (entry) => {
if (!/^word\/(document|header\d+|footer\d+|footnotes|endnotes)\.xml$/i.test(entry.fileName)) {
zipFile.readEntry();
return;
}
readEntry(zipFile, entry)
.then((xml) => {
documents.push({ fileName: entry.fileName, xml });
zipFile.readEntry();
})
.catch(reject);
});
zipFile.on('end', () => {
zipFile.close();
resolve(documents);
});
zipFile.on('error', reject);
});
}
function xmlToLines(xml) {
const withStructuralBreaks = String(xml || '')
.replace(/<w:tab\b[^>]*\/>/gi, '\t')
.replace(/<w:(?:br|cr)\b[^>]*\/>/gi, '\n')
.replace(/<\/w:p>/gi, '\n')
.replace(/<\/w:tr>/gi, '\n')
.replace(/<\/w:tc>/gi, '\t')
.replace(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/gi, (_, value) => decodeXml(value))
.replace(/<w:delText\b[^>]*>([\s\S]*?)<\/w:delText>/gi, (_, value) => decodeXml(value));
const textOnly = decodeXml(withStructuralBreaks.replace(/<[^>]+>/g, ' '));
return textOnly
.split(/\r?\n/)
.map((line) => line.replace(/[ \t]+/g, ' ').trim())
.filter((line) => line.length > 0);
}
async function extractDocxLines(docxPath) {
const entries = await readWordXmlEntries(docxPath);
const ordered = entries.sort((left, right) => left.fileName.localeCompare(right.fileName));
return ordered.flatMap((entry) => xmlToLines(entry.xml));
}
function buildLcsMatrix(left, right) {
const matrix = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
for (let leftIndex = left.length - 1; leftIndex >= 0; leftIndex -= 1) {
for (let rightIndex = right.length - 1; rightIndex >= 0; rightIndex -= 1) {
if (left[leftIndex] === right[rightIndex]) {
matrix[leftIndex][rightIndex] = matrix[leftIndex + 1][rightIndex + 1] + 1;
} else {
matrix[leftIndex][rightIndex] = Math.max(
matrix[leftIndex + 1][rightIndex],
matrix[leftIndex][rightIndex + 1],
);
}
}
}
return matrix;
}
function diffLines(previousLines, nextLines) {
const matrix = buildLcsMatrix(previousLines, nextLines);
const changes = [];
let previousIndex = 0;
let nextIndex = 0;
while (previousIndex < previousLines.length && nextIndex < nextLines.length) {
if (previousLines[previousIndex] === nextLines[nextIndex]) {
previousIndex += 1;
nextIndex += 1;
continue;
}
if (matrix[previousIndex + 1][nextIndex] >= matrix[previousIndex][nextIndex + 1]) {
changes.push({ type: 'removed', line: previousIndex + 1, text: previousLines[previousIndex] });
previousIndex += 1;
} else {
changes.push({ type: 'added', line: nextIndex + 1, text: nextLines[nextIndex] });
nextIndex += 1;
}
}
while (previousIndex < previousLines.length) {
changes.push({ type: 'removed', line: previousIndex + 1, text: previousLines[previousIndex] });
previousIndex += 1;
}
while (nextIndex < nextLines.length) {
changes.push({ type: 'added', line: nextIndex + 1, text: nextLines[nextIndex] });
nextIndex += 1;
}
return changes;
}
async function comparePair({ label, previous, next }) {
const previousExists = fs.existsSync(previous);
const nextExists = fs.existsSync(next);
if (!previousExists || !nextExists) {
throw new Error(`${label}: missing input file(s)`);
}
const [previousLines, nextLines] = await Promise.all([
extractDocxLines(previous),
extractDocxLines(next),
]);
const changes = diffLines(previousLines, nextLines);
return {
label,
previous,
next,
previousLineCount: previousLines.length,
nextLineCount: nextLines.length,
changes,
};
}
function printComparison(result) {
console.log(`\n=== ${result.label} ===`);
console.log(`Old: ${path.basename(result.previous)} (${result.previousLineCount} lines)`);
console.log(`New: ${path.basename(result.next)} (${result.nextLineCount} lines)`);
if (!result.changes.length) {
console.log('No textual differences detected.');
return;
}
console.log(`Detected ${result.changes.length} text-level changes:`);
result.changes.forEach((change) => {
const marker = change.type === 'added' ? '+' : '-';
console.log(`${marker} [${change.line}] ${change.text}`);
});
}
async function main() {
const results = [];
for (const comparison of COMPARISONS) {
results.push(await comparePair(comparison));
}
results.forEach(printComparison);
}
main().catch((error) => {
console.error('[compareAboContractDocxVersions] failed:', error?.message || error);
process.exitCode = 1;
});