Compare Changes at consolidate

This commit is contained in:
2026-02-26 16:18:45 +01:00
parent c8309832f7
commit 6190d707bb
8 changed files with 214 additions and 56 deletions

View File

@@ -6,17 +6,17 @@
<DefaultLocale>de-DE</DefaultLocale> <DefaultLocale>de-DE</DefaultLocale>
<DisplayName DefaultValue="SAT - Kabelliste Generator"/> <DisplayName DefaultValue="SAT - Kabelliste Generator"/>
<Description DefaultValue="Konsolidiert strukturierte Kabelzugdaten aus mehreren Tabellenblättern in eine Gesamtliste."/> <Description DefaultValue="Konsolidiert strukturierte Kabelzugdaten aus mehreren Tabellenblättern in eine Gesamtliste."/>
<IconUrl DefaultValue="https://kabel.casademm.de/assets/icon-32.png"/> <IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/>
<HighResolutionIconUrl DefaultValue="https://kabel.casademm.de/assets/icon-64.png"/> <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
<SupportUrl DefaultValue="https://kabel.casademm.de"/> <SupportUrl DefaultValue="https://localhost:3000/"/>
<AppDomains> <AppDomains>
<AppDomain>https://kabel.casademm.de</AppDomain> <AppDomain>https://localhost:3000/</AppDomain>
</AppDomains> </AppDomains>
<Hosts> <Hosts>
<Host Name="Workbook"/> <Host Name="Workbook"/>
</Hosts> </Hosts>
<DefaultSettings> <DefaultSettings>
<SourceLocation DefaultValue="https://kabel.casademm.de/taskpane.html"/> <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
</DefaultSettings> </DefaultSettings>
<Permissions>ReadWriteDocument</Permissions> <Permissions>ReadWriteDocument</Permissions>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0"> <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
@@ -62,14 +62,14 @@
</Hosts> </Hosts>
<Resources> <Resources>
<bt:Images> <bt:Images>
<bt:Image id="Icon.16x16" DefaultValue="https://kabel.casademm.de/assets/icon-16.png"/> <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="https://kabel.casademm.de/assets/icon-32.png"/> <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
<bt:Image id="Icon.80x80" DefaultValue="https://kabel.casademm.de/assets/icon-80.png"/> <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
</bt:Images> </bt:Images>
<bt:Urls> <bt:Urls>
<bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://kabel.casademm.de/help"/> <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://localhost:3000/help"/>
<bt:Url id="Commands.Url" DefaultValue="https://kabel.casademm.de/commands.html"/> <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
<bt:Url id="Taskpane.Url" DefaultValue="https://kabel.casademm.de/taskpane.html"/> <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
</bt:Urls> </bt:Urls>
<bt:ShortStrings> <bt:ShortStrings>
<bt:String id="GetStarted.Title" DefaultValue="Willkommen zum Kabel-Konsolidierungs-Tool!"/> <bt:String id="GetStarted.Title" DefaultValue="Willkommen zum Kabel-Konsolidierungs-Tool!"/>

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { makeStyles } from "@fluentui/react-components"; import { makeStyles } from "@fluentui/react-components";
import { SheetInfo, SheetMappingStatus } from "../models"; import { SheetInfo, SheetMappingStatus, ConsolidateSettings } from "../models";
import SheetSelector from "./SheetSelector"; import SheetSelector from "./SheetSelector";
import ColumnMapper from "./ColumnMapper"; import ColumnMapper from "./ColumnMapper";
import StatusNotifier from "./StatusNotifier"; import StatusNotifier from "./StatusNotifier";
@@ -11,6 +11,8 @@ interface AppProps {
title: string; title: string;
} }
declare const DEV_MODE: boolean;
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
minHeight: "100vh", minHeight: "100vh",
@@ -33,6 +35,12 @@ const App: React.FC<AppProps> = () => {
const [statusMessage, setStatusMessage] = useState(""); const [statusMessage, setStatusMessage] = useState("");
const [isConsolidating, setIsConsolidating] = useState(false); const [isConsolidating, setIsConsolidating] = useState(false);
const [settings, setSettings] = useState<ConsolidateSettings>({
colorNew: "#d4edda", // light green
colorChanged: "#fff3cd", // light yellow/orange
colorDeleted: "#f8d7da", // light red
});
useEffect(() => { useEffect(() => {
// Load internal sheets on mount // Load internal sheets on mount
getAvailableSheets().then(setSheets).catch(err => { getAvailableSheets().then(setSheets).catch(err => {
@@ -91,7 +99,7 @@ const App: React.FC<AppProps> = () => {
setIsConsolidating(true); setIsConsolidating(true);
setStatus("idle"); setStatus("idle");
try { try {
const rowsCount = await consolidateData(sheetMappings); const rowsCount = await consolidateData(sheetMappings, settings);
setStatus("success"); setStatus("success");
setStatusMessage(`Erfolgreich! Es wurden ${rowsCount} Zeilen aus ${sheetMappings.length} Blättern zusammengefasst.`); setStatusMessage(`Erfolgreich! Es wurden ${rowsCount} Zeilen aus ${sheetMappings.length} Blättern zusammengefasst.`);
setStep("done"); setStep("done");
@@ -105,6 +113,11 @@ const App: React.FC<AppProps> = () => {
return ( return (
<div className={styles.root}> <div className={styles.root}>
{typeof DEV_MODE !== "undefined" && DEV_MODE && (
<div style={{ backgroundColor: "#ffc107", color: "#000", textAlign: "center", padding: "4px", fontWeight: "bold", fontSize: "12px" }}>
DEV ENVIRONMENT
</div>
)}
<StatusNotifier status={status} message={statusMessage} /> <StatusNotifier status={status} message={statusMessage} />
{step === "select_sheets" && ( {step === "select_sheets" && (
@@ -120,6 +133,8 @@ const App: React.FC<AppProps> = () => {
{step === "map_columns" && ( {step === "map_columns" && (
<ColumnMapper <ColumnMapper
sheetMappings={sheetMappings} sheetMappings={sheetMappings}
settings={settings}
onSettingsChange={setSettings}
onHeaderRowChange={handleHeaderRowChange} onHeaderRowChange={handleHeaderRowChange}
onMappingChange={handleMappingChange} onMappingChange={handleMappingChange}
onBack={() => setStep("select_sheets")} onBack={() => setStep("select_sheets")}

View File

@@ -13,7 +13,7 @@ import {
Label, Label,
Spinner, Spinner,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { SheetMappingStatus, TARGET_COLUMNS } from "../models"; import { SheetMappingStatus, TARGET_COLUMNS, ConsolidateSettings } from "../models";
interface ColumnMapperProps { interface ColumnMapperProps {
sheetMappings: SheetMappingStatus[]; sheetMappings: SheetMappingStatus[];
@@ -22,6 +22,8 @@ interface ColumnMapperProps {
onBack: () => void; onBack: () => void;
onConsolidate: () => void; onConsolidate: () => void;
isConsolidating: boolean; isConsolidating: boolean;
settings: ConsolidateSettings;
onSettingsChange: (settings: ConsolidateSettings) => void;
} }
const ColumnMapper: React.FC<ColumnMapperProps> = ({ const ColumnMapper: React.FC<ColumnMapperProps> = ({
@@ -31,6 +33,8 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
onBack, onBack,
onConsolidate, onConsolidate,
isConsolidating, isConsolidating,
settings,
onSettingsChange,
}) => { }) => {
return ( return (
<div style={{ display: "flex", flexDirection: "column", gap: "15px", padding: "10px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "15px", padding: "10px" }}>
@@ -119,6 +123,36 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
})} })}
</Accordion> </Accordion>
<div style={{ marginTop: "20px", padding: "10px", backgroundColor: "#f3f2f1", borderRadius: "4px" }}>
<Text weight="semibold">Hervorhebungen (Farben)</Text>
<div style={{ display: "flex", gap: "20px", marginTop: "10px" }}>
<Field label="Neue Kabel">
<input
type="color"
value={settings.colorNew}
onChange={(e) => onSettingsChange({ ...settings, colorNew: e.target.value })}
style={{ width: "60px", padding: "0", cursor: "pointer", height: "30px", border: "none" }}
/>
</Field>
<Field label="Geänderte Werte">
<input
type="color"
value={settings.colorChanged}
onChange={(e) => onSettingsChange({ ...settings, colorChanged: e.target.value })}
style={{ width: "60px", padding: "0", cursor: "pointer", height: "30px", border: "none" }}
/>
</Field>
<Field label="Entfallene Kabel">
<input
type="color"
value={settings.colorDeleted}
onChange={(e) => onSettingsChange({ ...settings, colorDeleted: e.target.value })}
style={{ width: "60px", padding: "0", cursor: "pointer", height: "30px", border: "none" }}
/>
</Field>
</div>
</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: "20px" }}> <div style={{ display: "flex", justifyContent: "space-between", marginTop: "20px" }}>
<Button onClick={onBack} disabled={isConsolidating}>Zurück</Button> <Button onClick={onBack} disabled={isConsolidating}>Zurück</Button>
<Button <Button

View File

@@ -1,5 +1,5 @@
/* global Excel, console */ /* global Excel, console */
import { SheetInfo, SheetMappingStatus, TARGET_COLUMNS } from "./models"; import { SheetInfo, SheetMappingStatus, TARGET_COLUMNS, ConsolidateSettings } from "./models";
/** /**
* Holt alle sichtbaren Arbeitsblätter des aktuellen Workbooks, außer "Gesamtliste". * Holt alle sichtbaren Arbeitsblätter des aktuellen Workbooks, außer "Gesamtliste".
@@ -126,9 +126,9 @@ function buildSheetMappingStatus(sheetInfo: SheetInfo, headerRow: any[], rowInde
/** /**
* Führt die eigentliche Konsolidierung aus allen Arbeitsblättern durch und schreibt * Führt die eigentliche Konsolidierung aus allen Arbeitsblättern durch und schreibt
* das Ergebnis in das Blatt "Gesamtliste". * das Ergebnis in das Blatt "Gesamtliste" (jetzt "Kabelliste").
*/ */
export async function consolidateData(mappings: SheetMappingStatus[]): Promise<number> { export async function consolidateData(mappings: SheetMappingStatus[], settings: ConsolidateSettings): Promise<number> {
return Excel.run(async (context) => { return Excel.run(async (context) => {
let rowsConsolidated = 0; let rowsConsolidated = 0;
const finalData: any[][] = []; const finalData: any[][] = [];
@@ -197,59 +197,153 @@ export async function consolidateData(mappings: SheetMappingStatus[]): Promise<n
} }
} }
// Kabelliste erstellen oder überschreiben // Prüfen, ob "Kabelliste" existiert
let targetSheet: Excel.Worksheet; let targetSheet: Excel.Worksheet;
let listExists = false;
try { try {
targetSheet = context.workbook.worksheets.getItem("Kabelliste"); targetSheet = context.workbook.worksheets.getItem("Kabelliste");
// Falls sie existiert, zuerst löschen, um sie neu zu erstellen targetSheet.load("name");
targetSheet.delete();
await context.sync(); await context.sync();
listExists = true;
} catch (e) { } catch (e) {
// Ignorieren (Blatt existiert noch nicht) listExists = false;
} }
try { const fullHeaders = [...TARGET_COLUMNS.map(tc => tc.id), "Länge", "gezogen am", "von (Monteur)"];
targetSheet = context.workbook.worksheets.add("Kabelliste");
// Header Zeile schreiben if (listExists) {
const fullHeaders = [...TARGET_COLUMNS.map(tc => tc.id), "Länge", "gezogen am", "von (Monteur)"]; try {
// Existierende Liste aktualisieren
const table = targetSheet.tables.getItem("KonsolidierteKabel");
const bodyRange = table.getDataBodyRange();
bodyRange.load("values, rowCount, columnCount");
await context.sync();
// finalData hat fullHeaders.length Spalten const existingValues = bodyRange.values;
const totalRowsCount = finalData.length + 1; // +1 für Header
const totalColsCount = fullHeaders.length;
const targetRange = targetSheet.getRangeByIndexes(0, 0, totalRowsCount, totalColsCount); const formatChangedQueue: { row: number, col: number }[] = [];
const formatDeletedQueue: number[] = [];
const allValues = [fullHeaders, ...finalData]; const incomingMap = new Map<string, any[]>();
for (const row of finalData) {
const kNr = String(row[0] || "").trim();
if (kNr) {
incomingMap.set(kNr, row);
}
}
// Formatiere die Zielzellen als Text ("@"), BEVOR die Werte reingeschrieben werden, const newRows: any[][] = [];
// damit Excel nicht versucht, Datumsstrings als numerische Datumsformate umzuwandeln.
const formatArray: string[][] = []; // Update existing & mark deleted
for (let i = 0; i < totalRowsCount; i++) { for (let r = 0; r < existingValues.length; r++) {
formatArray.push(new Array(totalColsCount).fill("@")); const row = existingValues[r];
const kNr = String(row[0] || "").trim();
if (!kNr) continue;
if (incomingMap.has(kNr)) {
// Geändert prüfen
const inVals = incomingMap.get(kNr)!;
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 });
}
}
incomingMap.delete(kNr);
} else {
// Entfallen
// Optional die Zeile einfärben
formatDeletedQueue.push(r);
}
}
// Verbleibend = Neue Kabel
incomingMap.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();
// 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++) {
currentBody.getRow(newRowsStartIndex + i).format.fill.color = settings.colorNew;
}
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");
targetRange.numberFormat = formatArray; const totalRowsCount = finalData.length + 1; // +1 für Header
targetRange.values = allValues; const totalColsCount = fullHeaders.length;
await context.sync(); const targetRange = targetSheet.getRangeByIndexes(0, 0, totalRowsCount, totalColsCount);
const allValues = [fullHeaders, ...finalData];
// Als Tabelle formatieren const formatArray: string[][] = [];
const table = targetSheet.tables.add(targetRange, true /* hasHeaders */); for (let i = 0; i < totalRowsCount; i++) {
table.name = "KonsolidierteKabel"; formatArray.push(new Array(totalColsCount).fill("@"));
table.style = "TableStyleLight9"; }
table.showFilterButton = true;
// Spaltenbreite anpassen (AutoFit) targetRange.numberFormat = formatArray;
targetRange.format.autofitColumns(); targetRange.values = allValues;
await context.sync();
targetSheet.activate(); await context.sync();
return rowsConsolidated; const table = targetSheet.tables.add(targetRange, true /* hasHeaders */);
} catch (error) { table.name = "KonsolidierteKabel";
console.error("Fehler beim Erstellen der Kabelliste:", error); table.style = "TableStyleLight9";
throw new Error("Fehler beim Erstellen der 'Kabelliste'. Möglicherweise ist die Arbeitsmappe schreibgeschützt."); table.showFilterButton = true;
// Markiere komplett neu entfernt auf Userwunsch
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.");
}
} }
}); });
} }

View File

@@ -5,7 +5,13 @@ import { FluentProvider, webLightTheme } from "@fluentui/react-components";
/* global document, Office, module, require, HTMLElement */ /* global document, Office, module, require, HTMLElement */
const title = "SAT - Kabelliste Generator"; declare const DEV_MODE: boolean;
//if dev server is running set title to "SAT - Kabelliste Generator (Dev)"
let title = "SAT - Kabelliste Generator";
if (typeof DEV_MODE !== "undefined" && DEV_MODE) {
title = "SAT - Kabelliste Generator (Dev)";
}
const rootElement: HTMLElement | null = document.getElementById("container"); const rootElement: HTMLElement | null = document.getElementById("container");
const root = rootElement ? createRoot(rootElement) : undefined; const root = rootElement ? createRoot(rootElement) : undefined;

View File

@@ -25,6 +25,12 @@ export interface TargetColumnDef {
aliases: string[]; // e.g. ["k-nr.", "kabelnummer", "nr."] aliases: string[]; // e.g. ["k-nr.", "kabelnummer", "nr."]
} }
export interface ConsolidateSettings {
colorNew: string;
colorChanged: string;
colorDeleted: string;
}
export const TARGET_COLUMNS: TargetColumnDef[] = [ export const TARGET_COLUMNS: TargetColumnDef[] = [
{ id: "K-Nr.", aliases: ["k-nr.", "k-nr", "kabelnummer", "kabel", "nummer", "nr.", "nr"] }, { id: "K-Nr.", aliases: ["k-nr.", "k-nr", "kabelnummer", "kabel", "nummer", "nr.", "nr"] },
{ id: "Bezeichnung", aliases: ["bezeichnung", "name", "titel", "beschreibung"] }, { id: "Bezeichnung", aliases: ["bezeichnung", "name", "titel", "beschreibung"] },

View File

@@ -5,7 +5,7 @@ const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack"); const webpack = require("webpack");
const urlDev = "https://localhost:3037/"; const urlDev = "https://localhost:3000/";
const urlProd = "https://kabel.casademm.de/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION const urlProd = "https://kabel.casademm.de/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION
async function getHttpsOptions() { async function getHttpsOptions() {
@@ -93,6 +93,9 @@ module.exports = async (env, options) => {
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Promise: ["es6-promise", "Promise"], Promise: ["es6-promise", "Promise"],
}), }),
new webpack.DefinePlugin({
DEV_MODE: JSON.stringify(dev),
}),
], ],
devServer: { devServer: {
hot: true, hot: true,