14 Commits

Author SHA1 Message Date
Peacock
f035200aff docs: add Changelog for 1.0.1.1 release and ignore zips 2026-03-02 08:29:34 +01:00
Peacock
3a9140bc37 Fix formatting errors 2026-03-02 08:17:30 +01:00
Peacock
63cff880a5 feat: handle duplicates with file source info (v1.0.1.1) 2026-02-27 12:04:49 +01:00
Peacock
a76a6766a5 modified: manifest.prod.xml 2026-02-27 10:24:56 +01:00
Peacock
faf3245e7e update icon / logo 2026-02-27 10:16:25 +01:00
Peacock
022a119fe9 update Manifest 2026-02-27 09:57:27 +01:00
Peacock
d2392ef80d update docs and README 2026-02-27 09:57:16 +01:00
Peacock
02e90a4981 Add Copyright Footer 2026-02-27 09:56:44 +01:00
Peacock
6190d707bb Compare Changes at consolidate 2026-02-26 16:18:45 +01:00
Peacock
c8309832f7 rem dist/ 2026-02-25 15:41:51 +01:00
Peacock
80425582bc update gitignore 2026-02-25 15:39:13 +01:00
Peacock
87fc414182 fix warning manifest 2026-02-25 13:53:11 +01:00
Peacock
f66ba53385 Update gitignore 2026-02-25 13:52:53 +01:00
Peacock
f737121dc7 moved Readmes to docs/ 2026-02-25 13:52:19 +01:00
42 changed files with 425 additions and 369 deletions

BIN
.gitignore vendored

Binary file not shown.

18
Changelog.md Normal file
View File

@@ -0,0 +1,18 @@
# Release v1.0.1.1 - Duplikat-Behandlung & Stabilitätsupdates
## ✨ Neue Features
- **Duplikat-Erkennung:** Beim Konsolidieren werden doppelte Kabelnummern nun intelligent überführt. Statt einfachem Überschreiben werden überzählige Duplikate am Ender der Tabelle angefügt.
- **Herkunfts-Info:** Die neue Spalte "Bemerkung" zeigt für alle Duplikate den genauen Ursprung an (z.B. `Duplikat (aus: Dateiname.xlsx - Blattname)`).
- **Farbliche Hervorhebung:** Duplikate erhalten einen hellorangen Hintergrund (`#FFE6CC`), der auch beim erneuten Aktualisieren der Liste korrekt vergeben und zurückgesetzt wird.
## 🐛 Bugfixes
- **Crash bei Listen-Aktualisierung behoben:** Gelöstes Problem, bei dem Excel nach dem Umbenennen alter "Kabelliste"-Tabellenblätter wegen Tabellennamen-Konflikten (`KonsolidierteKabel`) den Dienst verweigerte. Das Add-In ist nun robust gegenüber manuell gesicherten/umbenannten Blättern.
- **Farben-Reset bei neuen Listen:** Ein `context.sync()`-Timing-Problem wurde behoben, durch das Duplikat-Farben beim initialen Erstellen einer Tabelle vom Standarddesign überschrieben wurden.
- **Fix für ungültige Farb-Hex-Werte:** Ein 8-stelliger (ungültiger) Farbcode für Duplikate wurde korrigiert, welcher einen Absturz (`InvalidArgument`) am Ende des Konsolidierungsprozesses verursachte.
## 💄 UI / UX
- Ein Farbwähler (Color-Picker) für Duplikate wurde in die UI integriert, analog zu "Neue Kabel", "Geändert" und "Entfallen".
- Die Fußzeile wurde um einen Copyright-Hinweis sowie direkte Links zum Herunterladen des **Produktions-Manifests** und der **Dokumentation** ergänzt.
## 📖 Dokumentation
- Neues Tutorial für lokales Sideloading via Netzwerkfreigabe (`05_Lokales_Prod_Sideloading.md`) zum Projekt hinzugefügt.

View File

@@ -40,6 +40,7 @@ Alle ausführlichen Leitfäden und Dokumentationen findest du im Ordner [`/docs`
2. **[Entwickler-Setup](./docs/02_Entwicklung_Setup.md)**: Wie setze ich das Projekt lokal auf und entwickle weiter? 2. **[Entwickler-Setup](./docs/02_Entwicklung_Setup.md)**: Wie setze ich das Projekt lokal auf und entwickle weiter?
3. **[Benutzerhandbuch](./docs/03_Benutzerhandbuch.md)**: Die Schritt-für-Schritt Anleitung für die Endnutzer. 3. **[Benutzerhandbuch](./docs/03_Benutzerhandbuch.md)**: Die Schritt-für-Schritt Anleitung für die Endnutzer.
4. **[Deployment & Hosting](./docs/04_Deployment.md)**: Wie kommt das Tool auf den Ubuntu-Server und in das M365 Admin Center? 4. **[Deployment & Hosting](./docs/04_Deployment.md)**: Wie kommt das Tool auf den Ubuntu-Server und in das M365 Admin Center?
5. **[Lokales Prod-Add-In (Sideloading)](./docs/05_Lokales_Prod_Sideloading.md)**: Wie lade ich das Produktions-Add-in in meiner lokalen Excel-App, wenn der Dev-Server nicht läuft?
--- ---

View File

@@ -1,130 +0,0 @@
# Deployment Guide: Ubuntu Server (Nginx + SSL)
Diese Anleitung beschreibt, wie du das fertig gebaute Excel Add-in auf deinem privaten Ubuntu-Server unter der Domain `https://kabel.casademm.de` hosten kannst.
## Voraussetzung
1. Ein Linux-Server (Ubuntu) mit Root/Sudo-Zugriff.
2. Die Domain `kabel.casademm.de` muss im DNS-Manager deines Domain-Anbieters auf die IP-Adresse (A-Record) dieses Servers zeigen.
3. Du hast lokal auf deinem Entwicklungsrechner den Befehl `npm run build` ausgeführt. Dadurch wurde ein Ordner namens `dist` in deinem Projektverzeichnis (`C:\EWSL_Add_in\CableConsolidation\dist`) erstellt.
---
## Schritt 1: Nginx installieren
Verbinde dich per SSH mit deinem Ubuntu-Server und aktualisiere die Paketquellen, um danach den Nginx Webserver zu installieren:
```bash
sudo apt update
sudo apt install nginx -y
```
Stelle sicher, dass Nginx läuft und beim Systemstart automatisch mitstartet:
```bash
sudo systemctl enable nginx
sudo systemctl start nginx
```
Falls du die `ufw` Firewall nutzt, erlaube den Nginx-Traffic:
```bash
sudo ufw allow 'Nginx Full'
```
---
## Schritt 2: Dateien auf den Server kopieren
1. Erstelle auf dem Server ein Verzeichnis für deine Domain:
```bash
sudo mkdir -p /var/www/kabel.casademm.de/html
```
2. Passe die Rechte an, damit Nginx (und dein User) darauf zugreifen können:
```bash
sudo chown -R $USER:$USER /var/www/kabel.casademm.de/html
sudo chmod -R 755 /var/www/kabel.casademm.de
```
3. Übertrage nun den Inhalt deines lokalen `dist`-Ordners in dieses Verzeichnis auf dem Server. Das kannst du z.B. über ein SFTP-Programm wie WinSCP oder FileZilla machen.
* **Quelle:** `C:\EWSL_Add_in\CableConsolidation\dist\*`
* **Ziel (Server):** `/var/www/kabel.casademm.de/html/`
*(Tipp: Vergewissere dich, dass die Datei `taskpane.html` und der `assets`-Ordner direkt im `/html/`-Verzeichnis liegen!)*
---
## Schritt 3: Nginx für die Domain konfigurieren
Erstelle eine neue Server-Block Konfigurationsdatei für Nginx:
```bash
sudo nano /etc/nginx/sites-available/kabel.casademm.de
```
Kopiere folgenden Inhalt hinein (dies lauscht erstmal nur auf Port 80):
```nginx
server {
listen 80;
listen [::]:80;
root /var/www/kabel.casademm.de/html;
index taskpane.html index.html index.htm;
server_name kabel.casademm.de;
location / {
try_files $uri $uri/ =404;
# Erlaubt CORS, was für Web-Add-Ins nützlich ist
add_header Access-Control-Allow-Origin *;
}
}
```
Speichere die Datei (in Nano: `Strg+O`, `Enter`, `Strg+X`).
Aktiviere die Konfiguration, indem du einen Symlink erstellst:
```bash
sudo ln -s /etc/nginx/sites-available/kabel.casademm.de /etc/nginx/sites-enabled/
```
Prüfe, ob Nginx meckert, und starte neu:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
---
## Schritt 4: SSL-Zertifikat sichern (WICHTIG!)
Microsoft Office weigert sich strikt, Add-ins ohne gültiges HTTPS-Zertifikat zu laden. Wir nutzen Certbot für ein kostenloses Let's Encrypt Zertifikat.
1. Installiere Certbot:
```bash
sudo apt install certbot python3-certbot-nginx -y
```
2. Generiere das Zertifikat:
```bash
sudo certbot --nginx -d kabel.casademm.de
```
Certbot wird dich nach deiner E-Mail-Adresse fragen und dir anbieten, den Traffic automatisch auf HTTPS umzuleiten (wähle Option "2: Redirect").
Sobald Certbot fertig ist, läuft dein Server sicher unter `https://kabel.casademm.de`.
---
## Schritt 5: In Excel einbinden / ausrollen
Jetzt wo dein Server online ist, benötigst du (und deine Firma) nur noch eine einzige Datei: Die `manifest.xml`.
In der `manifest.xml` (welche du ebenfalls in deinem Projekt-Root hast) stehen bereits alle Verweise auf `https://kabel.casademm.de`.
**Wie lade ich es im Office 365 Admin Center hoch?**
1. Die IT geht auf `admin.microsoft.com`.
2. Gehe zu **Einstellungen > Integrierte Apps**.
3. Klicke auf **Benutzerdefinierte Apps hochladen**.
4. Lade die finale `manifest.xml` hoch.
5. Weise die App den entsprechenden Benutzern (oder allen) zu.
6. Sobald Excel von den Mitarbeitern neugestartet wird, erscheint der neue Button im Menüband!
*(Für lokale Tests kannst du das Add-In in Excel auch einfach über "Meine Add-ins" > "Zusatz-Add-In hochladen" nutzen.)*

View File

@@ -1,51 +0,0 @@
# Kurzanleitung: Kabel-Konsolidierungs Add-In
Willkommen beim Kabel-Konsolidierungs Tool! Mit diesem Add-In kannst du schnell und effizient hunderte von Kabel-Einträgen aus verschiedenen Arbeitsblättern zu einer einzigen "Kabelliste" zusammenfassen.
## So startest du das Tool
1. Öffne Excel.
2. Wechsle im oberen Menüband (Ribbon) auf den Reiter **Start**.
3. Klicke ganz rechts auf den neuen Button **Start Konsolidierung** (mit dem Kabel-Symbol).
4. Das Add-In öffnet sich daraufhin am rechten Bildschirmrand.
---
## 1. Blätter auswählen & Dateien hochladen
Im ersten Schritt sagst du dem Add-In, *woher* es die Kabeldaten nehmen soll:
* **Interne Blätter:** Hier ist eine Liste aller sichtbaren Arbeitsblätter deines aktuell geöffneten Excel-Dokuments. Hake einfach alle Blätter an, die Kabeldaten enthalten.
* **Externe Excel-Dateien hinzufügen:** Wenn deine Kollegen dir weitere Excel-Listen (als `.xlsx`, `.xlsm` oder `.csv`) geschickt haben, musst du diese **nicht** manuell in deine aktuelle Mappe kopieren! Klicke einfach auf den Durchsuchen-Button und lade die Dateien direkt hoch. Das Add-In liest sie im Hintergrund aus und fügt auch diese Arbeitsblätter der Checkliste hinzu.
Klicke auf `Weiter`, wenn du alle gewünschten Blätter markiert hast.
---
## 2. Spalten-Mapping (Das Herzstück!)
Nun liest das Tool automatisch die ersten 50 Zeilen der gewählten Blätter aus und sucht nach den Kopfzeilen (Spaltennamen), die Kabel-Informationen beinhalten.
Das Add-In ist "schlau" und erkennt auch Abweichungen (es weiß z.B., dass "Kabelnummer" und "Nr." dasselbe meinen wie "K-Nr.").
* **Ein grünes "OK":** Das Tool hat alle nötigen Spalten für dieses Arbeitsblatt automatisch gefunden. Du musst hier nichts mehr tun.
* **Eine rote Warnung "X Lücken":** Für dieses Blatt konnten nicht alle Spalten automatisch gefunden werden.
* Klicke auf das Blatt, um die Details auszuklappen.
* Stelle sicher, dass unter **"Kopfzeile (Index 0-basiert)"** die korrekte Zeile angegeben ist, in der die Spaltenüberschriften stehen. Wenn deine Überschriften in Zeile 5 stehen, trage hier die "4" ein (da die Zählung bei 0 beginnt).
* Weise dann über das Dropdown-Menü manuell zu, welche Spalte im Dokument unserer Zielspalte (z.B. "von Raum") entspricht.
* Wenn ein Blatt komplett leer ist oder versehentlich ausgewählt wurde, lass die Spalten auf "--- Nicht gefunden ---". Das Tool überspringt diese Einträge am Ende ganz einfach, anstatt kaputtzugehen.
---
## 3. Konsolidierung
Sobald du zufrieden bist, klicke unten auf den blauen Button **"Konsolidieren"**.
Das Add-In wird nun:
1. Eine komplett neue Tabelle namens **"Kabelliste"** in deiner Arbeitsmappe erzeugen (bzw. eine alte überschreiben).
2. Alle Daten aus den internen UND externen Blättern in diese Tabelle schütten.
3. Die Tabelle schön und übersichtlich formatieren (inkl. Filter-Buttons).
4. Leere Zeilen automatisch überspringen und leere Spalten sauber auffüllen.
Fertig! Dir wird am Ende rechts angezeigt, wie viele Zeilen erfolgreich aus den Blättern konsolidiert wurden.
Du kannst das Add-In nun schließen oder einen neuen Durchlauf starten.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

1
dist/commands.html vendored
View File

@@ -1 +0,0 @@
@@ -1,18 +0,0 @@<!doctype html><html><head><meta charset="UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=Edge"/><script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script><script defer="defer" src="polyfill.js"></script><script defer="defer" src="commands.js"></script></head><body></body></html>

2
dist/commands.js vendored
View File

@@ -1,2 +0,0 @@
Office.onReady((function(){})),Office.actions.associate("action",(function(c){c.completed()}));
//# sourceMappingURL=commands.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"commands.js","mappings":"AAOAA,OAAOC,SAAQ,WACb,IAYFD,OAAOE,QAAQC,UAAU,UALzB,SAAgBC,GAEdA,EAAMC,WACR","sources":["webpack://office-addin-taskpane-react/./src/commands/commands.ts"],"sourcesContent":["/*\n * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n * See LICENSE in the project root for license information.\n */\n\n/* global Office */\n\nOffice.onReady(() => {\n // If needed, Office.js is ready to be called.\n});\n\n/**\n * Shows a notification when the add-in command is executed.\n * @param event\n */\nfunction action(event: Office.AddinCommands.Event) {\n // Your code here\n event.completed();\n}\n\nOffice.actions.associate(\"action\", action);\n"],"names":["Office","onReady","actions","associate","event","completed"],"sourceRoot":""}

3
dist/polyfill.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
/*!
* @overview es6-promise - a tiny implementation of Promises/A+.
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
* @license Licensed under MIT license
* See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
* @version v4.2.8+1e68dce6
*/

File diff suppressed because one or more lines are too long

3
dist/react.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,37 +0,0 @@
/*!
* @overview es6-promise - a tiny implementation of Promises/A+.
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
* @license Licensed under MIT license
* See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
* @version v4.2.8+1e68dce6
*/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

1
dist/react.js.map vendored

File diff suppressed because one or more lines are too long

1
dist/taskpane.html vendored
View File

@@ -1 +0,0 @@
<!doctype html><html lang="en" data-framework="typescript"><head><meta charset="UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=Edge"/><meta name="viewport" content="width=device-width,initial-scale=1"><title>Contoso Task Pane Add-in</title><script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script><script defer="defer" src="polyfill.js"></script><script defer="defer" src="react.js"></script><script defer="defer" src="taskpane.js"></script></head><body style="width:100%;height:100%;margin:0;padding:0"><div id="container"></div><div id="tridentmessage" style="display:none;padding:10">This add-in will not run in your version of Office. Please upgrade either to perpetual Office 2021 (or later) or to a Microsoft 365 account.</div><script>if(-1!==navigator.userAgent.indexOf("Trident")||-1!==navigator.userAgent.indexOf("Edge")){var tridentMessage=document.getElementById("tridentmessage"),normalContainer=document.getElementById("container");tridentMessage.style.display="block",normalContainer.style.display="none"}</script></body></html>

3
dist/taskpane.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,37 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/*! sheetjs (C) 2013-present SheetJS -- http://sheetjs.com */
/*! xlsx.js (C) 2013-present SheetJS -- http://sheetjs.com */
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,54 @@
# Lokales Testen des Produktions-Add-Ins (Sideloading)
Wenn der lokale Entwicklungs-Server (`npm start`) nicht läuft, kann das Produktions-Add-In (welches auf dem Live-Server gehostet wird) nicht einfach in der Excel Desktop-App über das Microsoft Admin-Center "hochgeladen" werden.
Um die Produktions-Version (`manifest.prod.xml`) dauerhaft lokal in Excel zu installieren, nutzt man unter Windows einen **freigegebenen Netzwerkordner (Shared Folder)**.
Folge dieser Schritt-für-Schritt-Anleitung, um dein lokales Excel für das Produktions-Add-In einzurichten:
## 1. Vorbereitung des Produktions-Manifests
1. Stelle sicher, dass du eine tagesaktuelle Version der `manifest.prod.xml` hast. Du kannst diese unter anderem **direkt aus dem entwickelten Add-In herunterladen**, wenn du es lokal laufen hast (Link unten im Add-In: *Prod-Manifest (.xml)*).
2. Diese Datei enthält die echten Produktions-URLs statt der `localhost`-Adressen.
*(Optional: Wenn du Dev- und Prod-Add-In gleichzeitig in Excel nutzen möchtest, stelle sicher, dass die `<Id>` in der `manifest.prod.xml` von der ID in der normalen `manifest.xml` abweicht.)*
## 2. Einen lokalen Ordner freigeben (Shared Folder)
Excel Desktop benötigt zwingend einen Netzwerkpfad (Share), um Manifeste lokal zu finden.
1. Erstelle irgendwo auf deinem PC einen neuen Ordner, z. B. `C:\ExcelManifests`.
2. Lege deine heruntergeladene `manifest.prod.xml` in diesen Ordner.
3. Klicke im Windows Explorer mit der **rechten Maustaste** auf den Ordner -> **Eigenschaften**.
4. Gehe zum Reiter **Freigabe** und klicke auf **Erweiterte Freigabe...**.
5. Setze den Haken bei **Diesen Ordner freigeben**. Merke dir den Freigabenamen (meist der Ordnername, z.B. `ExcelManifests`).
6. Klicke auf **OK** und schließe die Eigenschaften. Du solltest nun den Netzwerkpfad sehen (z. B. `\\DeinPCName\ExcelManifests` oder `\\localhost\ExcelManifests`).
## 3. Den Ordner in Excel als "Trusted Catalog" hinzufügen
Damit Excel diesen freigegebenen Ordner nach Manifesten durchsucht:
1. Öffne ein beliebiges lokales **Excel**.
2. Gehe auf **Datei** -> **Optionen** -> **Trust Center** (bzw. Sicherheitscenter).
3. Klicke auf den Button **Einstellungen für das Trust Center...**.
4. Wähle im linken Menü **Vertrauenswürdige Add-In-Kataloge** aus.
5. Trage unten bei **Katalog-URL** den Netzwerkpfad aus Schritt 2 ein (z. B. `\\localhost\ExcelManifests`).
6. Klicke auf **Katalog hinzufügen**.
7. **WICHTIG:** Setze den Haken bei **Im Menü anzeigen** (Show in Menu) für diesen neuen Eintrag!
8. Klicke auf **OK** und **starte Excel komplett neu**.
## 4. Das Produktions-Add-in in Excel laden
Nach dem Neustart von Excel:
1. Gehe im Menüband auf den Reiter **Einfügen** -> **Add-Ins abrufen** (Get Add-ins).
2. Oben im erscheinenden Dialogfenster siehst du nun einen neuen Reiter namens **FREIGEGEBENER ORDNER** (Shared Folder).
3. Klicke darauf. Dort taucht nun dein Produktions-Add-in auf.
4. Klicke auf **Hinzufügen**.
**Das war's!**
Ab sofort kannst du dein Produktions-Add-in ganz normal über das Menü starten, auch wenn dein lokaler Entwicklungs-Server aus ist.
### Updates des Add-Ins
Da die Excel Desktop-App bei jedem Start des Add-Ins die aktuellsten React/Web-Dateien von deinem Server lädt, musst du diesen Prozess **nicht** bei jedem Update wiederholen.
Du musst die `manifest.prod.xml` im Ordner `C:\ExcelManifests` nur dann aktualisieren (überschreiben) und das Add-In neu in Excel laden, wenn sich grundlegende Metadaten im Manifest ändern (wie z. B. der Name des Add-Ins, die URL, Icons oder angeforderte Berechtigungen).

BIN
docs/README.md Normal file

Binary file not shown.

View File

@@ -12,7 +12,7 @@
"full": "Macht aus mehreren Tabellenblättern eine Kabelliste" "full": "Macht aus mehreren Tabellenblättern eine Kabelliste"
}, },
"developer": { "developer": {
"name": "Toni Martin - SAT Elektrotechnik GmbH", "name": "T.M. - SAT Elektrotechnik GmbH",
"websiteUrl": "https://kabel.casademm.de", "websiteUrl": "https://kabel.casademm.de",
"privacyUrl": "https://kabel.casademm.de/privacy", "privacyUrl": "https://kabel.casademm.de/privacy",
"termsOfUseUrl": "https://kabel.casademm.de/terms" "termsOfUseUrl": "https://kabel.casademm.de/terms"

View File

@@ -1,22 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp"> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
<Id>2c37abde-33e4-4624-b95a-a0aed1526f1b</Id> <Id>2c37abde-33e4-4624-b95a-a0aed1526f1b</Id>
<Version>1.0.0.0</Version> <Version>1.0.1.1</Version>
<ProviderName>SAT Elektrotechnik GmbH</ProviderName> <ProviderName>SAT Elektrotechnik GmbH</ProviderName>
<DefaultLocale>de-DE</DefaultLocale> <DefaultLocale>de-DE</DefaultLocale>
<DisplayName DefaultValue="Kabel-Konsolidierung"/> <DisplayName DefaultValue="SAT - Kabelliste Generator (PROD)"/>
<Description DefaultValue="Konsolidiert strukturierte Kabeldaten 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,22 +62,22 @@
</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!"/>
<bt:String id="CommandsGroup.Label" DefaultValue="Funktionen"/> <bt:String id="CommandsGroup.Label" DefaultValue="SAT-Elektro"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Start Konsolidierung"/> <bt:String id="TaskpaneButton.Label" DefaultValue="Kabelliste generieren"/>
</bt:ShortStrings> </bt:ShortStrings>
<bt:LongStrings> <bt:LongStrings>
<bt:String id="GetStarted.Description" DefaultValue="Das Tool wurde geladen. Klicke im Menüband Start auf 'Start Konsolidierung', um zu beginnen."/> <bt:String id="GetStarted.Description" DefaultValue="Das Tool wurde geladen. Klicke im Menüband Start auf 'Kabelliste generieren', um zu beginnen."/>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Klicke hier, um das Tool zur Kabel-Konsolidierung zu öffnen"/> <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Klicke hier, um das Tool zur Kabel-Konsolidierung zu öffnen"/>
</bt:LongStrings> </bt:LongStrings>
</Resources> </Resources>

View File

@@ -1,22 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp"> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
<Id>2c37abde-33e4-4624-b95a-a0aed1526f1b</Id> <Id>2c37abde-33e4-4624-b95a-a0aed1526f1b</Id>
<Version>1.0.0.0</Version> <Version>1.0.1.1</Version>
<ProviderName>SAT Elektrotechnik GmbH</ProviderName> <ProviderName>SAT Elektrotechnik GmbH</ProviderName>
<DefaultLocale>de-DE</DefaultLocale> <DefaultLocale>de-DE</DefaultLocale>
<DisplayName DefaultValue="Kabel-Konsolidierung"/> <DisplayName DefaultValue="SAT - Kabelliste Generator (DEV)"/>
<Description DefaultValue="Konsolidiert strukturierte Kabeldaten 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,22 +62,22 @@
</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!"/>
<bt:String id="CommandsGroup.Label" DefaultValue="Funktionen"/> <bt:String id="CommandsGroup.Label" DefaultValue="DEV-SAT-Elektro"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Start Konsolidierung"/> <bt:String id="TaskpaneButton.Label" DefaultValue="DEV Kabelliste generieren"/>
</bt:ShortStrings> </bt:ShortStrings>
<bt:LongStrings> <bt:LongStrings>
<bt:String id="GetStarted.Description" DefaultValue="Das Tool wurde geladen. Klicke im Menüband Start auf 'Start Konsolidierung', um zu beginnen."/> <bt:String id="GetStarted.Description" DefaultValue="Das Tool wurde geladen. Klicke im Menüband Start auf 'Kabelliste generieren', um zu beginnen."/>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Klicke hier, um das Tool zur Kabel-Konsolidierung zu öffnen"/> <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Klicke hier, um das Tool zur Kabel-Konsolidierung zu öffnen"/>
</bt:LongStrings> </bt:LongStrings>
</Resources> </Resources>

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,13 @@ 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
colorDuplicate: "#ffe6cc", // light orange for duplicates
});
useEffect(() => { useEffect(() => {
// Load internal sheets on mount // Load internal sheets on mount
getAvailableSheets().then(setSheets).catch(err => { getAvailableSheets().then(setSheets).catch(err => {
@@ -91,7 +100,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 +114,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 +134,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")}
@@ -144,6 +160,17 @@ const App: React.FC<AppProps> = () => {
</button> </button>
</div> </div>
)} )}
{/* Footer Area with Links and Copyright */}
<div style={{ marginTop: "auto", padding: "10px", textAlign: "center", fontSize: "12px", borderTop: "1px solid #eee", width: "100%", boxSizing: "border-box" }}>
<div style={{ marginBottom: "8px" }}>
<a href="/manifest.prod.xml" download style={{ color: "#0078d4", textDecoration: "none", marginRight: "16px" }}>Prod-Manifest (.xml)</a>
<a href="/docs/" target="_blank" rel="noopener noreferrer" style={{ color: "#0078d4", textDecoration: "none" }}>Dokumentation</a>
</div>
<div style={{ color: "#666" }}>
&copy; {new Date().getFullYear()} Toni Martin - SAT Elektrotechnik GmbH. Alle Rechte vorbehalten.
</div>
</div>
</div> </div>
); );
}; };

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,44 @@ 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>
<Field label="Duplikate">
<input
type="color"
value={settings.colorDuplicate}
onChange={(e) => onSettingsChange({ ...settings, colorDuplicate: 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".
@@ -120,15 +120,16 @@ function buildSheetMappingStatus(sheetInfo: SheetInfo, headerRow: any[], rowInde
availableColumns, availableColumns,
mappings, mappings,
isExternal: sheetInfo.isExternal, isExternal: sheetInfo.isExternal,
fileName: sheetInfo.fileName,
externalData: sheetInfo.externalData externalData: sheetInfo.externalData
}; };
} }
/** /**
* 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[][] = [];
@@ -187,69 +188,239 @@ export async function consolidateData(mappings: SheetMappingStatus[]): Promise<n
} }
} }
// Zusatzfelder für die Kabelliste: Länge, gezogen am, von (Monteur) // Zusatzfelder für die Kabelliste: Länge, gezogen am, von (Monteur), Bemerkung
consolidatedRow.push(""); consolidatedRow.push("");
consolidatedRow.push(""); 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); finalData.push(consolidatedRow);
rowsConsolidated++; rowsConsolidated++;
} }
} }
// Kabelliste erstellen oder überschreiben // --- DUPLIKAT PRÜFUNG ---
let targetSheet: Excel.Worksheet; const cableCountMap = new Map<string, number>();
try { // Finde heraus, wie oft jede Kabelnummer vorkommt
targetSheet = context.workbook.worksheets.getItem("Kabelliste"); for (const row of finalData) {
// Falls sie existiert, zuerst löschen, um sie neu zu erstellen const kNr = String(row[0] || "").trim();
targetSheet.delete(); if (kNr) {
await context.sync(); cableCountMap.set(kNr, (cableCountMap.get(kNr) || 0) + 1);
} catch (e) { }
// Ignorieren (Blatt existiert noch nicht)
} }
try { // Schreibe "Duplikat" inkl. Quelle in die Bemerkungs-Spalte, ansonsten leere die Spalte wieder
targetSheet = context.workbook.worksheets.add("Kabelliste"); for (const row of finalData) {
const kNr = String(row[0] || "").trim();
const sourceInfo = row[row.length - 1]; // Temporär gespeicherte Info abrufen
// Header Zeile schreiben if (kNr && (cableCountMap.get(kNr) || 0) > 1) {
const fullHeaders = [...TARGET_COLUMNS.map(tc => tc.id), "Länge", "gezogen am", "von (Monteur)"]; row[row.length - 1] = `Duplikat (aus: ${sourceInfo})`;
} else {
// finalData hat fullHeaders.length Spalten row[row.length - 1] = ""; // Nur Duplikate erhalten einen Eintrag in "Bemerkung"
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("@"));
} }
}
// --- ENDE DUPLIKAT PRÜFUNG ---
targetRange.numberFormat = formatArray; // Prüfen, ob "Kabelliste" existiert
targetRange.values = allValues; let targetSheet: Excel.Worksheet;
let listExists = false;
try {
targetSheet = context.workbook.worksheets.getItem("Kabelliste");
targetSheet.load("name");
await context.sync(); await context.sync();
listExists = true;
} catch (e) {
listExists = false;
}
// Als Tabelle formatieren const fullHeaders = [...TARGET_COLUMNS.map(tc => tc.id), "Länge", "gezogen am", "von (Monteur)", "Bemerkung"];
const table = targetSheet.tables.add(targetRange, true /* hasHeaders */);
table.name = "KonsolidierteKabel";
table.style = "TableStyleLight9";
table.showFilterButton = true;
// Spaltenbreite anpassen (AutoFit) if (listExists) {
targetRange.format.autofitColumns(); try {
await context.sync(); // 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();
targetSheet.activate(); const existingValues = bodyRange.values;
return rowsConsolidated; const formatChangedQueue: { row: number, col: number }[] = [];
} catch (error) { const formatDeletedQueue: number[] = [];
console.error("Fehler beim Erstellen der Kabelliste:", error); const formatDuplicateQueue: number[] = []; // NEU: Queue für Duplikate
throw new Error("Fehler beim Erstellen der 'Kabelliste'. Möglicherweise ist die Arbeitsmappe schreibgeschützt.");
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);
}
} }
}); });
} }

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 = "Contoso Task Pane Add-in"; 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

@@ -17,6 +17,7 @@ export interface SheetMappingStatus {
mappings: ColumnMappingInfo[]; mappings: ColumnMappingInfo[];
availableColumns: { name: string; index: number }[]; availableColumns: { name: string; index: number }[];
isExternal?: boolean; isExternal?: boolean;
fileName?: string;
externalData?: any[][]; externalData?: any[][];
} }
@@ -25,6 +26,13 @@ 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;
colorDuplicate: 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() {
@@ -72,6 +72,11 @@ module.exports = async (env, options) => {
from: "assets/*", from: "assets/*",
to: "assets/[name][ext][query]", to: "assets/[name][ext][query]",
}, },
{
from: "docs/*",
to: "docs/[name][ext][query]",
noErrorOnMissing: true,
},
{ {
from: "manifest*.xml", from: "manifest*.xml",
to: "[name]" + "[ext]", to: "[name]" + "[ext]",
@@ -93,6 +98,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,