958 lines
29 KiB
TypeScript
958 lines
29 KiB
TypeScript
/* global Excel, console */
|
|
import { SheetInfo, SheetMappingStatus, TARGET_COLUMNS, ConsolidateSettings } from "./models";
|
|
|
|
/**
|
|
* Holt alle sichtbaren Arbeitsblätter des aktuellen Workbooks, außer "Gesamtliste".
|
|
*/
|
|
export async function getAvailableSheets(): Promise<SheetInfo[]> {
|
|
return Excel.run(async (context) => {
|
|
const sheets = context.workbook.worksheets;
|
|
sheets.load("items/name, items/visibility");
|
|
await context.sync();
|
|
|
|
return sheets.items
|
|
.filter((sheet) => sheet.name !== "Gesamtliste" && sheet.visibility === Excel.SheetVisibility.visible)
|
|
.map((sheet) => ({
|
|
id: sheet.name,
|
|
name: sheet.name,
|
|
}));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Extrahiert für ausgewählte Blätter die Kopfzeile und das Mapping-Ergebnis.
|
|
* Die Funktion durchsucht die ersten 50 Zeilen (inkl. leerer Zeilen),
|
|
* um die eigentlichen Spaltennamen zu finden.
|
|
*/
|
|
export async function detectHeadersAndColumns(sheets: SheetInfo[]): Promise<SheetMappingStatus[]> {
|
|
return Excel.run(async (context) => {
|
|
const results: SheetMappingStatus[] = [];
|
|
|
|
for (const sheetInfo of sheets) {
|
|
let values: any[][] = [];
|
|
|
|
if (sheetInfo.isExternal && sheetInfo.externalData) {
|
|
// Bei externen Dateien nehmen wir einfach die ersten 50 Zeilen aus den geparsten Daten
|
|
values = sheetInfo.externalData.slice(0, 50);
|
|
} else {
|
|
// Bei internen Dateien laden wir die Daten über die Excel API
|
|
const sheet = context.workbook.worksheets.getItem(sheetInfo.name);
|
|
const range = sheet.getRange("A1:AX50");
|
|
range.load("values");
|
|
await context.sync();
|
|
values = range.values;
|
|
}
|
|
|
|
let bestRowIndex = 0;
|
|
let maxMatches = -1;
|
|
|
|
// Finde die Zeile, die die meisten Übereinstimmungen mit unseren Zielspalten hat
|
|
for (let r = 0; r < values.length; r++) {
|
|
const rowData = values[r];
|
|
if (!rowData) continue; // Safety check in case external sheet has sparse arrays
|
|
|
|
let matches = 0;
|
|
for (let c = 0; c < rowData.length; c++) {
|
|
const cellStr = String(rowData[c]).trim().toLowerCase();
|
|
if (TARGET_COLUMNS.some((tc) => tc.aliases.includes(cellStr))) {
|
|
matches++;
|
|
}
|
|
}
|
|
if (matches > maxMatches) {
|
|
maxMatches = matches;
|
|
bestRowIndex = r;
|
|
}
|
|
}
|
|
|
|
// Sicherstellen, dass values[bestRowIndex] existiert (falls Blatt komplett leer ist)
|
|
// Wenn maxMatches immer noch -1 ist (oder 0), dann wurde absolut nichts gefunden
|
|
const headerRow = (maxMatches > 0 && values[bestRowIndex]) ? values[bestRowIndex] : [];
|
|
const status = buildSheetMappingStatus(sheetInfo, headerRow, maxMatches > 0 ? bestRowIndex : 0);
|
|
results.push(status);
|
|
}
|
|
|
|
return results;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Erlaubt das Neuladen für eine spezifische (vom Nutzer geänderte) Kopfzeile.
|
|
*/
|
|
export async function detectHeadersForSingleSheetRow(sheetInfo: SheetInfo, rowIndex: number): Promise<SheetMappingStatus> {
|
|
if (sheetInfo.isExternal && sheetInfo.externalData) {
|
|
// Externe Datei
|
|
const headerRow = sheetInfo.externalData[rowIndex] || [];
|
|
return buildSheetMappingStatus(sheetInfo, headerRow, rowIndex);
|
|
} else {
|
|
// Interne Datei
|
|
return Excel.run(async (context) => {
|
|
const sheet = context.workbook.worksheets.getItem(sheetInfo.name);
|
|
// rowIndex ist 0-basiert, Range ist 1-basiert
|
|
const range = sheet.getRangeByIndexes(rowIndex, 0, 1, 50); // Lese Spalten 0-49 in der gewünschten Zeile
|
|
range.load("values");
|
|
await context.sync();
|
|
|
|
return buildSheetMappingStatus(sheetInfo, range.values[0], rowIndex);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hilfsfunktion zum Erstellen des State-Objekts für das Mapping.
|
|
*/
|
|
function buildSheetMappingStatus(sheetInfo: SheetInfo, headerRow: any[], rowIndex: number): SheetMappingStatus {
|
|
const availableColumns = headerRow.map((val, idx) => ({
|
|
name: String(val).trim() || `(Leere Spalte ${idx + 1})`,
|
|
index: idx,
|
|
})).filter(col => col.name !== `(Leere Spalte ${col.index + 1})`);
|
|
|
|
const mappings = TARGET_COLUMNS.map((tc) => {
|
|
const foundIdx = headerRow.findIndex((cell) => tc.aliases.includes(String(cell).trim().toLowerCase()));
|
|
return {
|
|
targetColumn: tc.id,
|
|
sourceColumnIndex: foundIdx,
|
|
};
|
|
});
|
|
|
|
return {
|
|
sheetId: sheetInfo.id,
|
|
sheetName: sheetInfo.name, // Für Anzeige
|
|
headerRowIndex: rowIndex,
|
|
availableColumns,
|
|
mappings,
|
|
isExternal: sheetInfo.isExternal,
|
|
fileName: sheetInfo.fileName,
|
|
externalData: sheetInfo.externalData
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Führt die eigentliche Konsolidierung aus allen Arbeitsblättern durch und schreibt
|
|
* das Ergebnis in das Blatt "Gesamtliste" (jetzt "Kabelliste").
|
|
*/
|
|
export async function consolidateData(mappings: SheetMappingStatus[], settings: ConsolidateSettings): Promise<number> {
|
|
return Excel.run(async (context) => {
|
|
let rowsConsolidated = 0;
|
|
const finalData: any[][] = [];
|
|
|
|
// Daten auslesen und mergen
|
|
for (const mapping of mappings) {
|
|
if (mapping.mappings.length === 0) continue;
|
|
|
|
let textValues: any[][] = [];
|
|
let dataStartRowOffset = 0;
|
|
const targetIndicesMap = mapping.mappings.map(m => m.sourceColumnIndex);
|
|
|
|
if (mapping.isExternal && mapping.externalData) {
|
|
// Bei externen Dateien
|
|
textValues = mapping.externalData;
|
|
dataStartRowOffset = mapping.headerRowIndex + 1;
|
|
} else {
|
|
// Bei internen Dateien
|
|
const sheet = context.workbook.worksheets.getItem(mapping.sheetName);
|
|
const usedRange = sheet.getUsedRange();
|
|
usedRange.load(["rowIndex", "rowCount", "columnCount", "text"]);
|
|
await context.sync();
|
|
|
|
const startRowIdx = usedRange.rowIndex;
|
|
// Offset berechnen: Wie viele Zeilen nach dem Start des UsedRange liegt die erste Datenzeile?
|
|
dataStartRowOffset = (mapping.headerRowIndex + 1) - startRowIdx;
|
|
|
|
// Wenn die Daten unterhalb des UsedRange beginnen oder das Blatt leer ist
|
|
if (dataStartRowOffset >= usedRange.rowCount || usedRange.rowCount === 0) {
|
|
continue;
|
|
}
|
|
|
|
textValues = usedRange.text; // Text returns the exact formatted strings
|
|
}
|
|
|
|
// Schleife ab Zeile nach Header bis Ende
|
|
for (let r = Math.max(0, dataStartRowOffset); r < textValues.length; r++) {
|
|
const textRow = textValues[r] || [];
|
|
|
|
// Prüfen ob Row komplett leer ist (oder zumindest in den Mapped Columns leer)
|
|
const isRowEmpty = targetIndicesMap.every(srcColIdx => {
|
|
if (srcColIdx === -1) return true;
|
|
const val = textRow[srcColIdx];
|
|
return val === null || val === undefined || String(val).trim() === "";
|
|
});
|
|
|
|
if (isRowEmpty) continue;
|
|
|
|
const consolidatedRow: any[] = [];
|
|
// Die 6 Standard Target Columns
|
|
for (const srcColIdx of targetIndicesMap) {
|
|
if (srcColIdx !== -1 && srcColIdx < textRow.length) {
|
|
consolidatedRow.push(String(textRow[srcColIdx]));
|
|
} else {
|
|
consolidatedRow.push("");
|
|
}
|
|
}
|
|
|
|
// Zusatzfelder für die Kabelliste: Länge, gezogen am, von (Monteur), Bemerkung
|
|
consolidatedRow.push("");
|
|
consolidatedRow.push("");
|
|
consolidatedRow.push("");
|
|
|
|
// Wir speichern temporär die Quelle in der letzten Spalte, um sie später für Duplikate zu nutzen
|
|
const sourceInfo = mapping.isExternal ? `${mapping.fileName || 'Externe Datei'} - ${mapping.sheetName}` : mapping.sheetName;
|
|
consolidatedRow.push(sourceInfo);
|
|
|
|
finalData.push(consolidatedRow);
|
|
rowsConsolidated++;
|
|
}
|
|
}
|
|
|
|
// --- DUPLIKAT PRÜFUNG ---
|
|
const cableCountMap = new Map<string, number>();
|
|
// Finde heraus, wie oft jede Kabelnummer vorkommt
|
|
for (const row of finalData) {
|
|
const kNr = String(row[0] || "").trim();
|
|
if (kNr) {
|
|
cableCountMap.set(kNr, (cableCountMap.get(kNr) || 0) + 1);
|
|
}
|
|
}
|
|
|
|
// Schreibe "Duplikat" inkl. Quelle in die Bemerkungs-Spalte, ansonsten leere die Spalte wieder
|
|
for (const row of finalData) {
|
|
const kNr = String(row[0] || "").trim();
|
|
const sourceInfo = row[row.length - 1]; // Temporär gespeicherte Info abrufen
|
|
|
|
if (kNr && (cableCountMap.get(kNr) || 0) > 1) {
|
|
row[row.length - 1] = `Duplikat (aus: ${sourceInfo})`;
|
|
} else {
|
|
row[row.length - 1] = ""; // Nur Duplikate erhalten einen Eintrag in "Bemerkung"
|
|
}
|
|
}
|
|
// --- ENDE DUPLIKAT PRÜFUNG ---
|
|
|
|
// Prüfen, ob "Kabelliste" existiert
|
|
let targetSheet: Excel.Worksheet;
|
|
let listExists = false;
|
|
try {
|
|
targetSheet = context.workbook.worksheets.getItem("Kabelliste");
|
|
targetSheet.load("name");
|
|
await context.sync();
|
|
listExists = true;
|
|
} catch (e) {
|
|
listExists = false;
|
|
}
|
|
|
|
const fullHeaders = [...TARGET_COLUMNS.map(tc => tc.id), "Länge", "gezogen am", "von (Monteur)", "Bemerkung"];
|
|
|
|
if (listExists) {
|
|
try {
|
|
// Existierende Liste aktualisieren
|
|
// Wir nutzen getItemAt(0) statt eines festen Namens, da der Name "KonsolidierteKabel" bei umbenannten Sicherungskopien blockiert sein kann.
|
|
const table = targetSheet.tables.getItemAt(0);
|
|
const bodyRange = table.getDataBodyRange();
|
|
bodyRange.load("values, rowCount, columnCount");
|
|
await context.sync();
|
|
|
|
const existingValues = bodyRange.values;
|
|
|
|
const formatChangedQueue: { row: number, col: number }[] = [];
|
|
const formatDeletedQueue: number[] = [];
|
|
const formatDuplicateQueue: number[] = []; // NEU: Queue für Duplikate
|
|
|
|
const incomingMap = new Map<string, any[][]>();
|
|
for (const row of finalData) {
|
|
const kNr = String(row[0] || "").trim();
|
|
if (kNr) {
|
|
if (!incomingMap.has(kNr)) {
|
|
incomingMap.set(kNr, []);
|
|
}
|
|
incomingMap.get(kNr)!.push(row);
|
|
}
|
|
}
|
|
|
|
const newRows: any[][] = [];
|
|
|
|
// Update existing & mark deleted
|
|
for (let r = 0; r < existingValues.length; r++) {
|
|
const row = existingValues[r];
|
|
const kNr = String(row[0] || "").trim();
|
|
|
|
if (!kNr) continue;
|
|
|
|
if (incomingMap.has(kNr)) {
|
|
const inValsArray = incomingMap.get(kNr)!;
|
|
if (inValsArray.length > 0) {
|
|
const inVals = inValsArray.shift()!; // pop the first one
|
|
// Geändert prüfen
|
|
for (let c = 0; c < TARGET_COLUMNS.length; c++) {
|
|
const oldVal = String(row[c] || "").trim();
|
|
const newVal = String(inVals[c] || "").trim();
|
|
if (oldVal !== newVal) {
|
|
existingValues[r][c] = newVal;
|
|
formatChangedQueue.push({ row: r, col: c });
|
|
}
|
|
}
|
|
|
|
// Prüfen ob Bemerkung aktualisiert werden muss (z.B. neues Duplikat)
|
|
const bemerkungColIndex = fullHeaders.length - 1;
|
|
const inBemerkung = String(inVals[bemerkungColIndex] || "").trim();
|
|
existingValues[r][bemerkungColIndex] = inBemerkung;
|
|
|
|
if (inBemerkung.startsWith("Duplikat")) {
|
|
formatDuplicateQueue.push(r);
|
|
}
|
|
|
|
if (inValsArray.length === 0) {
|
|
incomingMap.delete(kNr);
|
|
}
|
|
} else {
|
|
// Should theoretically not happen if we delete when 0, but safe fallback
|
|
formatDeletedQueue.push(r);
|
|
}
|
|
} else {
|
|
// Entfallen
|
|
formatDeletedQueue.push(r);
|
|
}
|
|
}
|
|
|
|
// Verbleibend = Neue Kabel (und überschüssige Duplikate)
|
|
incomingMap.forEach((newRowsArray) => {
|
|
newRowsArray.forEach((newRow) => {
|
|
newRows.push(newRow);
|
|
});
|
|
});
|
|
|
|
// Werte in die bestehende Tabelle zurückschreiben
|
|
if (existingValues.length > 0) {
|
|
bodyRange.values = existingValues;
|
|
}
|
|
|
|
// Neue Zeilen anhängen
|
|
let newRowsStartIndex = existingValues.length;
|
|
if (newRows.length > 0) {
|
|
table.rows.add(null as any, newRows);
|
|
}
|
|
|
|
await context.sync();
|
|
|
|
// Formate anwenden
|
|
const currentBody = table.getDataBodyRange();
|
|
|
|
// Alte Formatierungen zurücksetzen, damit behobene Konflikte wieder normal aussehen
|
|
currentBody.format.fill.clear();
|
|
currentBody.format.font.strikethrough = false;
|
|
|
|
// Geänderte Zellen markieren
|
|
for (const cell of formatChangedQueue) {
|
|
currentBody.getCell(cell.row, cell.col).format.fill.color = settings.colorChanged;
|
|
}
|
|
|
|
// Entfallene Zeilen markieren
|
|
for (const rowIndex of formatDeletedQueue) {
|
|
currentBody.getRow(rowIndex).format.fill.color = settings.colorDeleted;
|
|
//Zellen durchgestrichen
|
|
currentBody.getRow(rowIndex).format.font.strikethrough = true;
|
|
}
|
|
|
|
// Neue Zeilen markieren
|
|
for (let i = 0; i < newRows.length; i++) {
|
|
const rowIndex = newRowsStartIndex + i;
|
|
const rowData = newRows[i];
|
|
|
|
if (String(rowData[fullHeaders.length - 1]).trim().startsWith("Duplikat")) {
|
|
currentBody.getRow(rowIndex).format.fill.color = settings.colorDuplicate;
|
|
} else {
|
|
currentBody.getRow(rowIndex).format.fill.color = settings.colorNew;
|
|
}
|
|
}
|
|
|
|
// Bestehende Duplikat-Zeilen markieren
|
|
for (const rowIndex of formatDuplicateQueue) {
|
|
currentBody.getRow(rowIndex).format.fill.color = settings.colorDuplicate;
|
|
}
|
|
|
|
table.getRange().format.autofitColumns();
|
|
await context.sync();
|
|
|
|
targetSheet.activate();
|
|
return rowsConsolidated;
|
|
} catch (error) {
|
|
console.error("Fehler beim Aktualisieren der Kabelliste:", error);
|
|
throw new Error("Fehler beim Aktualisieren der 'Kabelliste'.");
|
|
}
|
|
} else {
|
|
try {
|
|
// Neu erstellen (wie bisher)
|
|
targetSheet = context.workbook.worksheets.add("Kabelliste");
|
|
|
|
const totalRowsCount = finalData.length + 1; // +1 für Header
|
|
const totalColsCount = fullHeaders.length;
|
|
|
|
const targetRange = targetSheet.getRangeByIndexes(0, 0, totalRowsCount, totalColsCount);
|
|
const allValues = [fullHeaders, ...finalData];
|
|
|
|
const formatArray: string[][] = [];
|
|
for (let i = 0; i < totalRowsCount; i++) {
|
|
formatArray.push(new Array(totalColsCount).fill("@"));
|
|
}
|
|
|
|
targetRange.numberFormat = formatArray;
|
|
targetRange.values = allValues;
|
|
|
|
await context.sync();
|
|
|
|
const table = targetSheet.tables.add(targetRange, true /* hasHeaders */);
|
|
table.style = "TableStyleLight9";
|
|
table.showFilterButton = true;
|
|
|
|
// WICHTIG: Excel überschreibt manuelle Formate, wenn wir den TableStyle nicht zuerst synchronisieren!
|
|
await context.sync();
|
|
|
|
// Duplikate beim Neu-Erstellen einfärben:
|
|
const bodyRange = table.getDataBodyRange();
|
|
|
|
for (let i = 0; i < finalData.length; i++) {
|
|
if (String(finalData[i][fullHeaders.length - 1]).trim().startsWith("Duplikat")) {
|
|
bodyRange.getRow(i).format.fill.color = settings.colorDuplicate;
|
|
}
|
|
}
|
|
|
|
targetRange.format.autofitColumns();
|
|
await context.sync();
|
|
|
|
targetSheet.activate();
|
|
return rowsConsolidated;
|
|
} catch (error) {
|
|
console.error("Fehler beim Erstellen der Kabelliste:", error);
|
|
throw new Error("Fehler beim Erstellen der 'Kabelliste'. Möglicherweise ist die Arbeitsmappe schreibgeschützt.\n" + error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
|
|
* Neue Funktion für den Vergleich von zwei Listen
|
|
|
|
*/
|
|
|
|
export async function compareData(
|
|
|
|
oldMapping: SheetMappingStatus,
|
|
|
|
newMapping: SheetMappingStatus,
|
|
|
|
settings: ConsolidateSettings
|
|
|
|
): Promise<number> {
|
|
|
|
return Excel.run(async (context) => {
|
|
|
|
// Hilfsfunktion zum Laden von Daten aus einem Mapping
|
|
|
|
const loadData = async (mapping: SheetMappingStatus): Promise<any[][]> => {
|
|
|
|
if (mapping.isExternal && mapping.externalData) {
|
|
|
|
// Return data starting after the header row
|
|
|
|
return mapping.externalData.slice(mapping.headerRowIndex + 1);
|
|
|
|
} else {
|
|
|
|
const sheet = context.workbook.worksheets.getItem(mapping.sheetName);
|
|
|
|
const usedRange = sheet.getUsedRange();
|
|
|
|
usedRange.load(["rowIndex", "rowCount", "text"]);
|
|
|
|
await context.sync();
|
|
|
|
|
|
|
|
const startRowIdx = usedRange.rowIndex;
|
|
|
|
const dataStartRowOffset = (mapping.headerRowIndex + 1) - startRowIdx;
|
|
|
|
|
|
|
|
if (dataStartRowOffset >= usedRange.rowCount || usedRange.rowCount === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return data starting from the calculated offset
|
|
|
|
return usedRange.text.slice(dataStartRowOffset);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const oldData = await loadData(oldMapping);
|
|
|
|
const newData = await loadData(newMapping);
|
|
|
|
|
|
|
|
// K-Nr Indices finden
|
|
|
|
const getColIndex = (mapping: SheetMappingStatus, targetId: string) => {
|
|
|
|
const m = mapping.mappings.find(m => m.targetColumn === targetId);
|
|
|
|
return m ? m.sourceColumnIndex : -1;
|
|
|
|
};
|
|
|
|
|
|
|
|
const oldKNrIdx = getColIndex(oldMapping, "K-Nr.");
|
|
|
|
const newKNrIdx = getColIndex(newMapping, "K-Nr.");
|
|
|
|
|
|
|
|
if (oldKNrIdx === -1 || newKNrIdx === -1) {
|
|
|
|
throw new Error("K-Nr. Spalte muss in beiden Listen zugeordnet sein, um vergleichen zu können.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Zu vergleichende Spalten (ohne die Ausnahmen)
|
|
|
|
const columnsToCompare = TARGET_COLUMNS.filter(
|
|
|
|
tc => tc.id !== "K-Nr." && tc.id !== "Länge" && tc.id !== "gezogen am" && tc.id !== "von" // 'von' hier ist Monteur in Gesamtliste, aber TARGET_COLUMNS['von'] ist Start!
|
|
|
|
);
|
|
|
|
// Wir vergleichen ALLE gemappten Target-Columns, aber ignorieren (wie gefordert) Lnge/gezogen am/gezogen von,
|
|
|
|
// welche gar nicht in TARGET_COLUMNS sind! "von" und "nach" in TARGET_COLUMNS sind die Räume/Geräte.
|
|
|
|
// Das heißt wir ignorieren hier nichts extra aus TARGET_COLUMNS, da die spezifischen "gezogen am" etc Felder
|
|
|
|
// erst in der Consolidate Funktion hardcoded angehängt werden und gar nicht gemappt sind!
|
|
|
|
const allTargetCols = TARGET_COLUMNS.map(tc => tc.id);
|
|
|
|
|
|
|
|
// "von Raum" und "nach Raum" Indizes für Umverlegt-Check
|
|
|
|
const vonRaumId = "von Raum";
|
|
|
|
const nachRaumId = "nach Raum";
|
|
|
|
|
|
|
|
// Mapped Header für die Ausgabetabelle
|
|
|
|
const outputHeaders = ["K-Nr.", "Status", "Änderungsdetails", ...allTargetCols.filter(id => id !== "K-Nr.")];
|
|
|
|
|
|
|
|
// Maps erstellen
|
|
|
|
const oldMap = new Map<string, any[]>();
|
|
|
|
const oldDuplicates = new Set<string>();
|
|
|
|
|
|
|
|
for (const row of oldData) {
|
|
|
|
const knr = String(row[oldKNrIdx] || "").trim();
|
|
|
|
if (!knr) continue;
|
|
|
|
if (oldMap.has(knr)) {
|
|
|
|
oldDuplicates.add(knr);
|
|
|
|
}
|
|
|
|
oldMap.set(knr, row);
|
|
|
|
}
|
|
|
|
|
|
|
|
const newMap = new Map<string, any[]>();
|
|
|
|
const newDuplicates = new Set<string>();
|
|
|
|
|
|
|
|
for (const row of newData) {
|
|
|
|
const knr = String(row[newKNrIdx] || "").trim();
|
|
|
|
if (!knr) continue;
|
|
|
|
if (newMap.has(knr)) {
|
|
|
|
newDuplicates.add(knr);
|
|
|
|
}
|
|
|
|
newMap.set(knr, row);
|
|
|
|
}
|
|
|
|
|
|
|
|
const finalOutput: any[][] = [];
|
|
|
|
const formatQueue: { row: number, state: "added" | "removed" | "changed" | "moved", formatColRanges?: number[] }[] = [];
|
|
|
|
let currentRowIndex = 1; // 1-based, header is 0
|
|
|
|
|
|
|
|
// 1. Check old list against new list (Removed / Changed / Moved)
|
|
|
|
oldMap.forEach((oldRow, knr) => {
|
|
|
|
if (!newMap.has(knr)) {
|
|
|
|
// Entfernt
|
|
|
|
const outRow = [knr, "Entfernt", "Kabel fehlt in der neuen Liste"];
|
|
|
|
for (const colId of allTargetCols) {
|
|
|
|
if (colId === "K-Nr.") continue;
|
|
|
|
const idx = getColIndex(oldMapping, colId);
|
|
|
|
outRow.push(idx !== -1 ? oldRow[idx] : "");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (oldDuplicates.has(knr)) outRow[2] += " (Duplikat in alter Liste)";
|
|
|
|
|
|
|
|
finalOutput.push(outRow);
|
|
|
|
formatQueue.push({ row: currentRowIndex, state: "removed" });
|
|
|
|
currentRowIndex++;
|
|
|
|
} else {
|
|
|
|
// Existiert in beiden -> Vergleichen
|
|
|
|
const newRow = newMap.get(knr)!;
|
|
|
|
let isChanged = false;
|
|
|
|
let isMoved = false;
|
|
|
|
const changes: string[] = [];
|
|
|
|
const changedCols: number[] = [];
|
|
|
|
|
|
|
|
for (const colId of allTargetCols) {
|
|
|
|
if (colId === "K-Nr.") continue;
|
|
|
|
|
|
|
|
const oIdx = getColIndex(oldMapping, colId);
|
|
|
|
const nIdx = getColIndex(newMapping, colId);
|
|
|
|
|
|
|
|
const oVal = oIdx !== -1 ? String(oldRow[oIdx] || "").trim() : "";
|
|
|
|
const nVal = nIdx !== -1 ? String(newRow[nIdx] || "").trim() : "";
|
|
|
|
|
|
|
|
if (oVal !== nVal) {
|
|
|
|
isChanged = true;
|
|
|
|
changes.push(`${colId}: ${oVal || "Leer"} -> ${nVal || "Leer"}`);
|
|
|
|
|
|
|
|
// Output Header mapping: 0=KNr, 1=Status, 2=Details, 3... = the rest (offset by 3)
|
|
|
|
const outputColIdx = outputHeaders.indexOf(colId);
|
|
|
|
if (outputColIdx !== -1) {
|
|
|
|
changedCols.push(outputColIdx);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (colId === vonRaumId || colId === nachRaumId) {
|
|
|
|
isMoved = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isChanged) {
|
|
|
|
const status = isMoved ? "Umverlegt" : "Geändert";
|
|
|
|
let details = changes.join(" | ");
|
|
|
|
|
|
|
|
if (oldDuplicates.has(knr) || newDuplicates.has(knr)) {
|
|
|
|
details += " (Duplikat gefunden!)";
|
|
|
|
}
|
|
|
|
|
|
|
|
const outRow = [knr, status, details];
|
|
|
|
for (const colId of allTargetCols) {
|
|
|
|
if (colId === "K-Nr.") continue;
|
|
|
|
const idx = getColIndex(newMapping, colId); // Zeigen die NEUEN Werte!
|
|
|
|
outRow.push(idx !== -1 ? newRow[idx] : "");
|
|
|
|
}
|
|
|
|
finalOutput.push(outRow);
|
|
|
|
formatQueue.push({ row: currentRowIndex, state: isMoved ? "moved" : "changed", formatColRanges: changedCols });
|
|
|
|
currentRowIndex++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 2. Check new list against old list (Added)
|
|
|
|
newMap.forEach((newRow, knr) => {
|
|
|
|
if (!oldMap.has(knr)) {
|
|
|
|
// Hinzugefügt
|
|
|
|
const outRow = [knr, "Neu", "Kabel neu hinzugefügt"];
|
|
|
|
for (const colId of allTargetCols) {
|
|
|
|
if (colId === "K-Nr.") continue;
|
|
|
|
const idx = getColIndex(newMapping, colId);
|
|
|
|
outRow.push(idx !== -1 ? newRow[idx] : "");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newDuplicates.has(knr)) outRow[2] += " (Duplikat in neuer Liste)";
|
|
|
|
|
|
|
|
finalOutput.push(outRow);
|
|
|
|
formatQueue.push({ row: currentRowIndex, state: "added" });
|
|
|
|
currentRowIndex++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 3. Wenn keine Änderungen
|
|
|
|
if (finalOutput.length === 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 4. In "Änderungsdokumentation" schreiben
|
|
|
|
const sheetName = "Änderungsdokumentation";
|
|
|
|
let targetSheet: Excel.Worksheet;
|
|
|
|
|
|
|
|
try {
|
|
|
|
targetSheet = context.workbook.worksheets.getItem(sheetName);
|
|
|
|
targetSheet.delete(); // Delete if exists to create a fresh one
|
|
|
|
await context.sync();
|
|
|
|
} catch (e) {
|
|
|
|
// Does not exist
|
|
|
|
}
|
|
|
|
|
|
|
|
targetSheet = context.workbook.worksheets.add(sheetName);
|
|
|
|
|
|
|
|
const totalRowsCount = finalOutput.length + 1; // +1 für Header
|
|
|
|
const totalColsCount = outputHeaders.length;
|
|
|
|
|
|
|
|
const targetRange = targetSheet.getRangeByIndexes(0, 0, totalRowsCount, totalColsCount);
|
|
|
|
const allValues = [outputHeaders, ...finalOutput];
|
|
|
|
|
|
|
|
const formatArray: string[][] = [];
|
|
|
|
for (let i = 0; i < totalRowsCount; i++) {
|
|
|
|
formatArray.push(new Array(totalColsCount).fill("@"));
|
|
|
|
}
|
|
|
|
|
|
|
|
targetRange.numberFormat = formatArray;
|
|
|
|
targetRange.values = allValues;
|
|
|
|
|
|
|
|
await context.sync();
|
|
|
|
|
|
|
|
const table = targetSheet.tables.add(targetRange, true /* hasHeaders */);
|
|
|
|
table.style = "TableStyleLight9";
|
|
|
|
table.showFilterButton = true;
|
|
|
|
|
|
|
|
await context.sync();
|
|
|
|
const bodyRange = table.getDataBodyRange();
|
|
|
|
|
|
|
|
// 5. Formate anwenden
|
|
|
|
for (const fmt of formatQueue) {
|
|
|
|
// row is 1-based (from data), bodyRange getRow is 0-based
|
|
|
|
const excelRow = bodyRange.getRow(fmt.row - 1);
|
|
|
|
|
|
|
|
if (fmt.state === "removed") {
|
|
|
|
excelRow.format.fill.color = settings.colorDeleted;
|
|
|
|
excelRow.format.font.strikethrough = true;
|
|
|
|
excelRow.format.font.color = "#990000"; // Dunkelrot
|
|
|
|
} else if (fmt.state === "added") {
|
|
|
|
excelRow.format.fill.color = settings.colorNew;
|
|
|
|
} else if (fmt.state === "moved") {
|
|
|
|
// Umverlegt z.B. hellblau oder lila (wir nehmen ein vordefiniertes Gelb oder passen settings an, hardcoded für jetzt)
|
|
|
|
excelRow.format.fill.color = "#d9edf7"; // Light Blue
|
|
|
|
|
|
|
|
// Einzelne Zellen markieren
|
|
|
|
if (fmt.formatColRanges) {
|
|
|
|
for (const col of fmt.formatColRanges) {
|
|
|
|
bodyRange.getCell(fmt.row - 1, col).format.fill.color = "#bce8f1"; // Stronger Blue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (fmt.state === "changed") {
|
|
|
|
excelRow.format.fill.color = settings.colorChanged;
|
|
|
|
|
|
|
|
// Einzelne Zellen markieren
|
|
|
|
if (fmt.formatColRanges) {
|
|
|
|
for (const col of fmt.formatColRanges) {
|
|
|
|
bodyRange.getCell(fmt.row - 1, col).format.fill.color = "#ffe699"; // Stronger Yellow
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
targetRange.format.autofitColumns();
|
|
|
|
await context.sync();
|
|
|
|
|
|
|
|
targetSheet.activate();
|
|
|
|
return finalOutput.length;
|
|
|
|
});
|
|
|
|
}
|
|
|