Initial Project
This commit is contained in:
255
src/taskpane/excelLogic.ts
Normal file
255
src/taskpane/excelLogic.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/* global Excel, console */
|
||||
import { SheetInfo, SheetMappingStatus, TARGET_COLUMNS } 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 {
|
||||
sheetName: sheetInfo.name, // Für Anzeige
|
||||
headerRowIndex: rowIndex,
|
||||
availableColumns,
|
||||
mappings,
|
||||
isExternal: sheetInfo.isExternal,
|
||||
externalData: sheetInfo.externalData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die eigentliche Konsolidierung aus allen Arbeitsblättern durch und schreibt
|
||||
* das Ergebnis in das Blatt "Gesamtliste".
|
||||
*/
|
||||
export async function consolidateData(mappings: SheetMappingStatus[]): 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)
|
||||
consolidatedRow.push("");
|
||||
consolidatedRow.push("");
|
||||
consolidatedRow.push("");
|
||||
|
||||
finalData.push(consolidatedRow);
|
||||
rowsConsolidated++;
|
||||
}
|
||||
}
|
||||
|
||||
// Kabelliste erstellen oder überschreiben
|
||||
let targetSheet: Excel.Worksheet;
|
||||
try {
|
||||
targetSheet = context.workbook.worksheets.getItem("Kabelliste");
|
||||
// Falls sie existiert, zuerst löschen, um sie neu zu erstellen
|
||||
targetSheet.delete();
|
||||
await context.sync();
|
||||
} catch (e) {
|
||||
// Ignorieren (Blatt existiert noch nicht)
|
||||
}
|
||||
|
||||
try {
|
||||
targetSheet = context.workbook.worksheets.add("Kabelliste");
|
||||
|
||||
// Header Zeile schreiben
|
||||
const fullHeaders = [...TARGET_COLUMNS.map(tc => tc.id), "Länge", "gezogen am", "von (Monteur)"];
|
||||
|
||||
// finalData hat fullHeaders.length Spalten
|
||||
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];
|
||||
|
||||
// Formatiere die Zielzellen als Text ("@"), BEVOR die Werte reingeschrieben werden,
|
||||
// damit Excel nicht versucht, Datumsstrings als numerische Datumsformate umzuwandeln.
|
||||
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();
|
||||
|
||||
// Als Tabelle formatieren
|
||||
const table = targetSheet.tables.add(targetRange, true /* hasHeaders */);
|
||||
table.name = "KonsolidierteKabel";
|
||||
table.style = "TableStyleLight9";
|
||||
table.showFilterButton = true;
|
||||
|
||||
// Spaltenbreite anpassen (AutoFit)
|
||||
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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user