Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b60c0ffd3 | ||
|
|
186e836edc | ||
|
|
30fc5a55fc | ||
|
|
7b14c36df0 | ||
|
|
3e444ae6a2 | ||
|
|
6410a28109 | ||
|
|
812d76e44c | ||
|
|
ae90228011 | ||
|
|
f035200aff | ||
|
|
3a9140bc37 | ||
|
|
63cff880a5 | ||
|
|
a76a6766a5 | ||
|
|
faf3245e7e | ||
|
|
022a119fe9 | ||
|
|
d2392ef80d | ||
|
|
02e90a4981 | ||
|
|
6190d707bb | ||
|
|
c8309832f7 | ||
|
|
80425582bc | ||
|
|
87fc414182 | ||
|
|
f66ba53385 | ||
|
|
f737121dc7 |
14
.gitignore
vendored
@@ -1 +1,13 @@
|
|||||||
node_modules
|
# Ignore environment files
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Ignore dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Ignore build output
|
||||||
|
dist/
|
||||||
|
dist/*
|
||||||
|
CableConsolidation-v1.0.1.1.zip
|
||||||
|
site/*
|
||||||
|
site/
|
||||||
|
docs/manifest.prod.xml
|
||||||
|
|||||||
34
Changelog.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# Release v1.0.2.0 - Listenabgleich (Vergleichen-Modus)
|
||||||
|
|
||||||
|
## ✨ Neue Features
|
||||||
|
- **Listenabgleich (Vergleichen-Modus):** Es ist nun möglich, zwei Listen (Alt vs. Neu) miteinander zu vergleichen, um herauszufinden, welche Kabel hinzugefügt, entfernt oder geändert wurden. Die Ausgabe erfolgt in ein neues Tabellenblatt "Änderungsdokumentation".
|
||||||
|
- **UI-Verbesserungen:**
|
||||||
|
- **Wizard-Überarbeitung:** Der Wizard wurde überarbeitet, um zwischen "Zusammenfassen" und "Vergleichen" zu wechseln.
|
||||||
|
- **Tab-Navigation:** Neue Tabs für "Vergleichen" und "Änderungsdokumentation" wurden hinzugefügt.
|
||||||
|
- **Spaltenauswahl:** Die Spaltenauswahl im Wizard wurde für den Vergleichsmodus angepasst.
|
||||||
|
|
||||||
|
## 🐛 Bugfixes
|
||||||
|
- **Mapping-Fehler behoben:** Gelöstes Problem, bei dem Listen mit identischem Blattnamen aus verschiedenen externen Dateien nicht korrekt zugeordnet wurden. Das Add-In verwendet nun intern eindeutige `sheetId`s anstelle von Blattnamen für die Zuordnung.
|
||||||
|
- **Fehlende Spalten im Vergleich:** Korrigiertes Verhalten, bei dem die Spalten "von Raum" und "nach Raum" im Vergleichsmodus nicht korrekt verarbeitet wurden.
|
||||||
|
|
||||||
|
## 📖 Dokumentation
|
||||||
|
- **Aktualisierte Anleitungen:** Die Dokumentation wurde aktualisiert, um die neuen Funktionen des Listenabgleichs zu beschreiben.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Cable Consolidation (Excel Add-In)
|
# Cable Consolidation (Excel Add-In)
|
||||||
|
|
||||||
Ein **Microsoft Excel Web-Add-In**, entwickelt von der SAT Elektrotechnik GmbH, zur intelligenten und effizienten Zusammenführung von Kabeldaten aus verschiedenen Tabellenblättern in eine formatierte "Kabelliste".
|
Ein **Microsoft Excel Web-Add-In**, entwickelt von der SAT Elektrotechnik GmbH, zur intelligenten und effizienten Zusammenführung von Kabeldaten aus verschiedenen Tabellenblättern in eine formatierte "Kabelliste".
|
||||||
|
|
||||||
@@ -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?
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -58,4 +59,6 @@ npm start
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
*Entwickelt mit ❤️ für eine effizientere Kabelplanung.*
|
© 2026 Toni Martin - [SAT Elektrotechnik GmbH](https://www.sat-elektro.de/)
|
||||||
|
---
|
||||||
|
*Unterstützt durch KI-Assistenten für den Gebrauch im Unternehmen*
|
||||||
@@ -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.)*
|
|
||||||
@@ -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.
|
|
||||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 812 B After Width: | Height: | Size: 706 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
16
deploy_docs.ps1
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
$server = "root@systems.casademm.de"
|
||||||
|
$destination = "/var/www/docs.casademm.de/html/"
|
||||||
|
|
||||||
|
Write-Host " Building MkDocs site..."
|
||||||
|
mkdocs build
|
||||||
|
|
||||||
|
Write-Host " Deploying Site and XML to docs.casademm.de..."
|
||||||
|
Write-Host " (Please provide your SSH password when prompted)"
|
||||||
|
|
||||||
|
# Upload site contents
|
||||||
|
scp -r ./site/* ${server}:${destination}
|
||||||
|
|
||||||
|
# Upload manifest.prod.xml
|
||||||
|
scp ./manifest.prod.xml ${server}:${destination}
|
||||||
|
|
||||||
|
Write-Host " Deployment Complete!"
|
||||||
BIN
dist/assets/icon-128.png
vendored
|
Before Width: | Height: | Size: 6.0 KiB |
BIN
dist/assets/icon-16.png
vendored
|
Before Width: | Height: | Size: 812 B |
BIN
dist/assets/icon-32.png
vendored
|
Before Width: | Height: | Size: 1.3 KiB |
BIN
dist/assets/icon-64.png
vendored
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
dist/assets/icon-80.png
vendored
|
Before Width: | Height: | Size: 4.1 KiB |
BIN
dist/assets/logo-filled.png
vendored
|
Before Width: | Height: | Size: 10 KiB |
1
dist/commands.html
vendored
@@ -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
@@ -1,2 +0,0 @@
|
|||||||
Office.onReady((function(){})),Office.actions.associate("action",(function(c){c.completed()}));
|
|
||||||
//# sourceMappingURL=commands.js.map
|
|
||||||
1
dist/commands.js.map
vendored
@@ -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
7
dist/polyfill.js.LICENSE.txt
vendored
@@ -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
|
|
||||||
*/
|
|
||||||
1
dist/polyfill.js.map
vendored
3
dist/react.js
vendored
37
dist/react.js.LICENSE.txt
vendored
@@ -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
1
dist/taskpane.html
vendored
@@ -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
37
dist/taskpane.js.LICENSE.txt
vendored
@@ -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.
|
|
||||||
*/
|
|
||||||
1
dist/taskpane.js.map
vendored
54
docs/05_Lokales_Prod_Sideloading.md
Normal 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).
|
||||||
52
docs/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Cable Consolidation (Excel Add-In)
|
||||||
|
|
||||||
|
Ein **Microsoft Excel Web-Add-In**, entwickelt von der SAT Elektrotechnik GmbH, zur intelligenten und effizienten Zusammenführung von Kabeldaten aus verschiedenen Tabellenblättern in eine formatierte "Kabelliste".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
Das Add-In optimiert den Planungsprozess durch folgende Funktionen:
|
||||||
|
- **Interne & Externe Quellen:** Konsolidiert Tabellenblätter aus der *aktuell geöffneten* Arbeitsmappe sowie aus *extern hochgeladenen* Excel-Dateien (`.xlsx`, `.xlsm`, `.csv`).
|
||||||
|
- **Intelligentes Mapping (Smart Aliasing):** Erkennt selbstständig die benötigten Spalten (z.B. "K-Nr.", "von", "nach Raum", etc.), auch wenn alternative Bezeichnungen (wie "Kabelnummer" oder "Nr.") verwendet werden.
|
||||||
|
- **Fehlertoleranz:** Leere Blätter, versteckte Blätter oder fehlende Kopfzeilen bringen das Add-In nicht zum Absturz. Der Benutzer kann fehlende Zuweisungen manuell vornehmen.
|
||||||
|
- **Saubere Ausgabe:** Die konsolidierten Daten werden automatisch als ansprechende, filterbare Excel-Tabelle generiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Technologie-Stack
|
||||||
|
Das Projekt nutzt modernste Web-Technologien in Kombination mit Microsofts Add-In Architektur:
|
||||||
|
- **React (v18)** für die dynamische Benutzeroberfläche (Task Pane).
|
||||||
|
- **TypeScript** für typsichere Geschäftslogik.
|
||||||
|
- **Fluent UI (v9)** für ein nahtloses, Microsoft-natives Design.
|
||||||
|
- **Office.js API** zum direkten Lesen und Schreiben im Excel-Dokument.
|
||||||
|
- **SheetJS (xlsx)** zum clientseitigen Parsen von externen Dateien direkt im Browser-Speicher.
|
||||||
|
- **Webpack** als Build-Tool.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
Wenn du direkt in den Code einsteigen willst:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Lokalen Entwicklungs-Server starten & Excel Add-In Sideloaden
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Produktions-Build erstellen
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
*Entwickelt mit ❤️ für eine effizientere Kabelplanung.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Changelog (Letzte Releases)
|
||||||
|
|
||||||
|
### v1.0.2.0 (Aktuell)
|
||||||
|
- **Neu: Listenabgleich (Vergleichen-Modus):** Es ist nun möglich, zwei Listen (Alt vs. Neu) miteinander zu vergleichen, um herauszufinden, welche Kabel hinzugefügt, entfernt oder geändert wurden. Die Ausgabe erfolgt in ein neues Tabellenblatt "Änderungsdokumentation".
|
||||||
|
- **Bugfix:** Listen mit identischem Blattnamen aus verschiedenen externen Dateien führen nicht mehr zu Zuordnungsfehlern im Mapping. Die App nutzt intern nun eindeutige `sheetId`s.
|
||||||
|
- **UI Update:** Der Wizard und die Tab-Navigation wurden überarbeitet, um zwischen Zusammenfassen und Vergleichen zu wechseln.
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.2.0</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://docs.casademm.de/"/>
|
||||||
<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>
|
||||||
34
manifest.xml
@@ -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.2.0</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>
|
||||||
|
|||||||
25
mkdocs.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
site_name: Cable Consolidation Add-in Docs
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
palette:
|
||||||
|
# Palette toggle for light mode
|
||||||
|
- scheme: default
|
||||||
|
primary: indigo
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
# Palette toggle for dark mode
|
||||||
|
- scheme: slate
|
||||||
|
primary: indigo
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to light mode
|
||||||
|
docs_dir: docs
|
||||||
|
nav:
|
||||||
|
- Home: README.md
|
||||||
|
- Architektur: 01_Architektur.md
|
||||||
|
- Entwicklungs-Setup: 02_Entwicklung_Setup.md
|
||||||
|
- Benutzerhandbuch: 03_Benutzerhandbuch.md
|
||||||
|
- Deployment: 04_Deployment.md
|
||||||
|
- Lokales Sideloading: 05_Lokales_Prod_Sideloading.md
|
||||||
|
- "📥 Prod-XML herunterladen": manifest.prod.xml
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "office-addin-taskpane-react",
|
"name": "office-addin-taskpane-react",
|
||||||
"version": "0.0.1",
|
"version": "1.0.2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-React.git"
|
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-React.git"
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
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, TabList, Tab } 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";
|
||||||
|
import CompareWizard from "./CompareWizard";
|
||||||
import { getAvailableSheets, detectHeadersAndColumns, detectHeadersForSingleSheetRow, consolidateData } from "../excelLogic";
|
import { getAvailableSheets, detectHeadersAndColumns, detectHeadersForSingleSheetRow, consolidateData } from "../excelLogic";
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare const DEV_MODE: boolean;
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
@@ -22,6 +25,9 @@ const useStyles = makeStyles({
|
|||||||
const App: React.FC<AppProps> = () => {
|
const App: React.FC<AppProps> = () => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
|
type AppMode = "consolidate" | "compare";
|
||||||
|
const [appMode, setAppMode] = useState<AppMode>("consolidate");
|
||||||
|
|
||||||
type WizardStep = "select_sheets" | "map_columns" | "done";
|
type WizardStep = "select_sheets" | "map_columns" | "done";
|
||||||
|
|
||||||
const [step, setStep] = useState<WizardStep>("select_sheets");
|
const [step, setStep] = useState<WizardStep>("select_sheets");
|
||||||
@@ -33,6 +39,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 => {
|
||||||
@@ -61,24 +74,24 @@ const App: React.FC<AppProps> = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHeaderRowChange = async (sheetName: string, newRowIndex: number) => {
|
const handleHeaderRowChange = async (sheetId: string, newRowIndex: number) => {
|
||||||
try {
|
try {
|
||||||
// Wir müssen das richtige SheetInfo Objekt finden (für isExternal check)
|
// Wir müssen das richtige SheetInfo Objekt finden (für isExternal check)
|
||||||
const sheetInfo = sheets.find(s => s.name === sheetName);
|
const sheetInfo = sheets.find(s => (s.id || s.name) === sheetId);
|
||||||
if (!sheetInfo) return;
|
if (!sheetInfo) return;
|
||||||
|
|
||||||
// Re-detect columns for exactly this row
|
// Re-detect columns for exactly this row
|
||||||
const newMapping = await detectHeadersForSingleSheetRow(sheetInfo, newRowIndex);
|
const newMapping = await detectHeadersForSingleSheetRow(sheetInfo, newRowIndex);
|
||||||
setSheetMappings(prev => prev.map(m => m.sheetName === sheetName ? newMapping : m));
|
setSheetMappings(prev => prev.map(m => (m.sheetId || m.sheetName) === sheetId ? newMapping : m));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
setStatusMessage("Fehler beim Neuladen der Zeile " + newRowIndex + " für Blatt " + sheetName);
|
setStatusMessage("Fehler beim Neuladen der Zeile " + newRowIndex + " für Blatt " + sheetId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMappingChange = (sheetName: string, targetCol: string, sourceColIndex: number) => {
|
const handleMappingChange = (sheetId: string, targetCol: string, sourceColIndex: number) => {
|
||||||
setSheetMappings(prev => prev.map(sheet => {
|
setSheetMappings(prev => prev.map(sheet => {
|
||||||
if (sheet.sheetName !== sheetName) return sheet;
|
if ((sheet.sheetId || sheet.sheetName) !== sheetId) return sheet;
|
||||||
|
|
||||||
const newMappings = sheet.mappings.map(m =>
|
const newMappings = sheet.mappings.map(m =>
|
||||||
m.targetColumn === targetCol ? { ...m, sourceColumnIndex: sourceColIndex } : m
|
m.targetColumn === targetCol ? { ...m, sourceColumnIndex: sourceColIndex } : m
|
||||||
@@ -91,7 +104,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,45 +118,86 @@ const App: React.FC<AppProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<StatusNotifier status={status} message={statusMessage} />
|
{typeof DEV_MODE !== "undefined" && DEV_MODE && (
|
||||||
|
<div style={{ backgroundColor: "#ffc107", color: "#000", textAlign: "center", padding: "4px", fontWeight: "bold", fontSize: "12px" }}>
|
||||||
{step === "select_sheets" && (
|
DEV ENVIRONMENT
|
||||||
<SheetSelector
|
|
||||||
sheets={sheets}
|
|
||||||
selectedSheetIds={selectedSheetIds}
|
|
||||||
onSelectionChange={setSelectedSheetIds}
|
|
||||||
onNext={handleNextToMapping}
|
|
||||||
onExternalSheetsLoaded={handleExternalSheetsLoaded}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === "map_columns" && (
|
|
||||||
<ColumnMapper
|
|
||||||
sheetMappings={sheetMappings}
|
|
||||||
onHeaderRowChange={handleHeaderRowChange}
|
|
||||||
onMappingChange={handleMappingChange}
|
|
||||||
onBack={() => setStep("select_sheets")}
|
|
||||||
onConsolidate={handleConsolidate}
|
|
||||||
isConsolidating={isConsolidating}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === "done" && (
|
|
||||||
<div style={{ padding: "10px", textAlign: "center", marginTop: "40px" }}>
|
|
||||||
<h2>Fertig!</h2>
|
|
||||||
<p>Die Daten wurden in die 'Gesamtliste' geschrieben.</p>
|
|
||||||
<button
|
|
||||||
style={{ padding: "8px 16px", marginTop: "10px", cursor: "pointer" }}
|
|
||||||
onClick={() => {
|
|
||||||
setStep("select_sheets");
|
|
||||||
setSelectedSheetIds([]);
|
|
||||||
setStatus("idle");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Neuen Durchlauf starten
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div style={{ padding: "10px", borderBottom: "1px solid #eee", marginBottom: "10px" }}>
|
||||||
|
<TabList
|
||||||
|
selectedValue={appMode}
|
||||||
|
onTabSelect={(_, data) => setAppMode(data.value as AppMode)}
|
||||||
|
>
|
||||||
|
<Tab value="consolidate">Zusammenfassen</Tab>
|
||||||
|
<Tab value="compare">Vergleichen</Tab>
|
||||||
|
</TabList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusNotifier status={status} message={statusMessage} />
|
||||||
|
|
||||||
|
{appMode === "consolidate" && (
|
||||||
|
<>
|
||||||
|
{step === "select_sheets" && (
|
||||||
|
<SheetSelector
|
||||||
|
sheets={sheets}
|
||||||
|
selectedSheetIds={selectedSheetIds}
|
||||||
|
onSelectionChange={setSelectedSheetIds}
|
||||||
|
onNext={handleNextToMapping}
|
||||||
|
onExternalSheetsLoaded={handleExternalSheetsLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "map_columns" && (
|
||||||
|
<ColumnMapper
|
||||||
|
sheetMappings={sheetMappings}
|
||||||
|
settings={settings}
|
||||||
|
onSettingsChange={setSettings}
|
||||||
|
onHeaderRowChange={handleHeaderRowChange}
|
||||||
|
onMappingChange={handleMappingChange}
|
||||||
|
onBack={() => setStep("select_sheets")}
|
||||||
|
onConsolidate={handleConsolidate}
|
||||||
|
isConsolidating={isConsolidating}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "done" && (
|
||||||
|
<div style={{ padding: "10px", textAlign: "center", marginTop: "40px" }}>
|
||||||
|
<h2>Fertig!</h2>
|
||||||
|
<p>Die Daten wurden in die 'Gesamtliste' geschrieben.</p>
|
||||||
|
<button
|
||||||
|
style={{ padding: "8px 16px", marginTop: "10px", cursor: "pointer" }}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("select_sheets");
|
||||||
|
setSelectedSheetIds([]);
|
||||||
|
setStatus("idle");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Neuen Durchlauf starten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{appMode === "compare" && (
|
||||||
|
<CompareWizard
|
||||||
|
availableSheets={sheets}
|
||||||
|
onExternalSheetsLoaded={handleExternalSheetsLoaded}
|
||||||
|
globalSettings={settings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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="https://docs.casademm.de/manifest.prod.xml" download style={{ color: "#0078d4", textDecoration: "none", marginRight: "16px" }}>Prod-Manifest (.xml)</a>
|
||||||
|
<a href="https://docs.casademm.de/" target="_blank" rel="noopener noreferrer" style={{ color: "#0078d4", textDecoration: "none" }}>Dokumentation</a>
|
||||||
|
</div>
|
||||||
|
<div style={{ color: "#666" }}>
|
||||||
|
© {new Date().getFullYear()} Toni Martin - SAT Elektrotechnik GmbH. Alle Rechte vorbehalten.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,15 +13,21 @@ 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[];
|
||||||
onHeaderRowChange: (sheetName: string, newRowIndex: number) => void;
|
onHeaderRowChange: (sheetIdentifier: string, newRowIndex: number) => void;
|
||||||
onMappingChange: (sheetName: string, targetCol: string, sourceColIndex: number) => void;
|
onMappingChange: (sheetIdentifier: string, targetCol: string, sourceColIndex: number) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onConsolidate: () => void;
|
onConsolidate: () => void;
|
||||||
isConsolidating: boolean;
|
isConsolidating: boolean;
|
||||||
|
settings: ConsolidateSettings;
|
||||||
|
onSettingsChange: (settings: ConsolidateSettings) => void;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
actionLoadingLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
||||||
@@ -31,22 +37,29 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
|||||||
onBack,
|
onBack,
|
||||||
onConsolidate,
|
onConsolidate,
|
||||||
isConsolidating,
|
isConsolidating,
|
||||||
|
settings,
|
||||||
|
onSettingsChange,
|
||||||
|
title = "2. Spalten-Mapping prüfen",
|
||||||
|
description = "Bitte überprüfe die gefundenen Kopfzeilen und passe fehlende Spalten manuell an.",
|
||||||
|
actionLabel = "Konsolidieren",
|
||||||
|
actionLoadingLabel = "Konsolidierung läuft...",
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "15px", padding: "10px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "15px", padding: "10px" }}>
|
||||||
<Text size={400} weight="semibold">
|
<Text size={400} weight="semibold">
|
||||||
2. Spalten-Mapping prüfen
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size={300}>
|
<Text size={300}>
|
||||||
Bitte überprüfe die gefundenen Kopfzeilen und passe fehlende Spalten manuell an.
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Accordion multiple collapsible defaultOpenItems={sheetMappings.map((s) => s.sheetName)}>
|
<Accordion multiple collapsible defaultOpenItems={sheetMappings.map((s) => s.sheetId || s.sheetName)}>
|
||||||
{sheetMappings.map((sheet) => {
|
{sheetMappings.map((sheet) => {
|
||||||
|
const identifier = sheet.sheetId || sheet.sheetName;
|
||||||
const missingCount = sheet.mappings.filter((m) => m.sourceColumnIndex === -1).length;
|
const missingCount = sheet.mappings.filter((m) => m.sourceColumnIndex === -1).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccordionItem key={sheet.sheetName} value={sheet.sheetName}>
|
<AccordionItem key={identifier} value={identifier}>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
|
||||||
<Text weight="semibold">{sheet.sheetName}</Text>
|
<Text weight="semibold">{sheet.sheetName}</Text>
|
||||||
@@ -75,7 +88,7 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
|||||||
min={0}
|
min={0}
|
||||||
onChange={(_, data) => {
|
onChange={(_, data) => {
|
||||||
if (data.value !== undefined) {
|
if (data.value !== undefined) {
|
||||||
onHeaderRowChange(sheet.sheetName, data.value);
|
onHeaderRowChange(identifier, data.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{ width: "80px" }}
|
style={{ width: "80px" }}
|
||||||
@@ -98,7 +111,7 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
|||||||
selectedOptions={[selectedVal || "-1"]}
|
selectedOptions={[selectedVal || "-1"]}
|
||||||
onOptionSelect={(_, data) => {
|
onOptionSelect={(_, data) => {
|
||||||
const newIndex = parseInt(data.optionValue || "-1", 10);
|
const newIndex = parseInt(data.optionValue || "-1", 10);
|
||||||
onMappingChange(sheet.sheetName, colName, newIndex);
|
onMappingChange(identifier, colName, newIndex);
|
||||||
}}
|
}}
|
||||||
style={{ minWidth: "150px" }}
|
style={{ minWidth: "150px" }}
|
||||||
>
|
>
|
||||||
@@ -119,6 +132,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
|
||||||
@@ -127,7 +178,7 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
|||||||
disabled={isConsolidating || sheetMappings.length === 0}
|
disabled={isConsolidating || sheetMappings.length === 0}
|
||||||
icon={isConsolidating ? <Spinner size="tiny" /> : undefined}
|
icon={isConsolidating ? <Spinner size="tiny" /> : undefined}
|
||||||
>
|
>
|
||||||
{isConsolidating ? "Konsolidierung läuft..." : "Konsolidieren"}
|
{isConsolidating ? actionLoadingLabel : actionLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
221
src/taskpane/components/CompareWizard.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { makeStyles, Button, Dropdown, Option, Label, Card, CardHeader } from "@fluentui/react-components";
|
||||||
|
import { SheetInfo, ConsolidateSettings, SheetMappingStatus } from "../models";
|
||||||
|
import { detectHeadersAndColumns, compareData } from "../excelLogic";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
import ColumnMapper from "./ColumnMapper";
|
||||||
|
|
||||||
|
interface CompareWizardProps {
|
||||||
|
availableSheets: SheetInfo[];
|
||||||
|
onExternalSheetsLoaded: (sheets: SheetInfo[]) => void;
|
||||||
|
globalSettings: ConsolidateSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
padding: "10px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "15px",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
maxWidth: "400px",
|
||||||
|
},
|
||||||
|
fileInput: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type CompareStep = "select_lists" | "map_columns" | "done";
|
||||||
|
|
||||||
|
const CompareWizard: React.FC<CompareWizardProps> = ({ availableSheets, onExternalSheetsLoaded, globalSettings }) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const [step, setStep] = useState<CompareStep>("select_lists");
|
||||||
|
|
||||||
|
const [oldSheetId, setOldSheetId] = useState<string>("");
|
||||||
|
const [newSheetId, setNewSheetId] = useState<string>("");
|
||||||
|
|
||||||
|
const [sheetMappings, setSheetMappings] = useState<SheetMappingStatus[]>([]);
|
||||||
|
const [isComparing, setIsComparing] = useState(false);
|
||||||
|
|
||||||
|
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const data = e.target?.result;
|
||||||
|
if (data) {
|
||||||
|
const workbook = XLSX.read(data, { type: "binary" });
|
||||||
|
const newSheets: SheetInfo[] = [];
|
||||||
|
|
||||||
|
workbook.SheetNames.forEach((sheetName) => {
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
const jsonData = XLSX.utils.sheet_to_json<any[]>(worksheet, { header: 1 });
|
||||||
|
|
||||||
|
newSheets.push({
|
||||||
|
id: `ext_${Date.now()}_${sheetName}`,
|
||||||
|
name: sheetName,
|
||||||
|
isExternal: true,
|
||||||
|
fileName: file.name,
|
||||||
|
externalData: jsonData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
onExternalSheetsLoaded(newSheets);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsBinaryString(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextToMapping = async () => {
|
||||||
|
if (!oldSheetId || !newSheetId) return;
|
||||||
|
|
||||||
|
const oldSheet = availableSheets.find(s => s.id === oldSheetId);
|
||||||
|
const newSheet = availableSheets.find(s => s.id === newSheetId);
|
||||||
|
|
||||||
|
if (!oldSheet || !newSheet) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// detectHeadersAndColumns needs an array of sheets and returns mappings for them
|
||||||
|
const mappings = await detectHeadersAndColumns([oldSheet, newSheet]);
|
||||||
|
setSheetMappings(mappings);
|
||||||
|
setStep("map_columns");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Fehler beim Erkennen der Spalten.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecuteComparison = async () => {
|
||||||
|
setIsComparing(true);
|
||||||
|
try {
|
||||||
|
const oldSheet = availableSheets.find(s => s.id === oldSheetId);
|
||||||
|
const newSheet = availableSheets.find(s => s.id === newSheetId);
|
||||||
|
|
||||||
|
console.log("oldSheetId:", oldSheetId, "newSheetId:", newSheetId);
|
||||||
|
console.log("Mappings vorhanden für:", sheetMappings.map(m => m.sheetId || m.sheetName));
|
||||||
|
|
||||||
|
// Suche mapping über id
|
||||||
|
const oldMapping = sheetMappings.find(m => (m.sheetId || m.sheetName) === oldSheetId);
|
||||||
|
const newMapping = sheetMappings.find(m => (m.sheetId || m.sheetName) === newSheetId);
|
||||||
|
|
||||||
|
if (!oldMapping || !newMapping) {
|
||||||
|
console.error("Old Mapping found:", !!oldMapping, "New Mapping found:", !!newMapping);
|
||||||
|
throw new Error(`Mappings für die ausgewählten Listen nicht gefunden. Erwartet: ${oldSheet?.name}, ${newSheet?.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsCompared = await compareData(oldMapping, newMapping, globalSettings);
|
||||||
|
alert(`Vergleich abgeschlossen! ${rowsCompared} Änderungen dokumentiert.`);
|
||||||
|
setStep("done");
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.message || "Fehler beim Vergleichen der Listen. Details in Console.");
|
||||||
|
} finally {
|
||||||
|
setIsComparing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provide a safe way to select if multiple sheets with same name but different files exist
|
||||||
|
const renderOptions = () => {
|
||||||
|
return availableSheets.map(s => {
|
||||||
|
const label = s.isExternal ? `${s.name} (aus ${s.fileName})` : s.name;
|
||||||
|
return <Option key={s.id} value={s.id}>{label}</Option>;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step === "map_columns") {
|
||||||
|
// Reuse ColumnMapper but pass specific logic (or hide settings for now)
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<ColumnMapper
|
||||||
|
sheetMappings={sheetMappings}
|
||||||
|
settings={globalSettings}
|
||||||
|
onSettingsChange={() => { }} // Not needed for comparing usually, or handled differently
|
||||||
|
onHeaderRowChange={() => {
|
||||||
|
// Re-detect logic here (similar to App.tsx) - will implement later if needed
|
||||||
|
}}
|
||||||
|
onMappingChange={(sheetId, targetCol, sourceColIndex) => {
|
||||||
|
const newMappings = [...sheetMappings];
|
||||||
|
const sheetIdx = newMappings.findIndex(m => (m.sheetId || m.sheetName) === sheetId);
|
||||||
|
if (sheetIdx > -1) {
|
||||||
|
const mappingIdx = newMappings[sheetIdx].mappings.findIndex(m => m.targetColumn === targetCol);
|
||||||
|
if (mappingIdx > -1) {
|
||||||
|
newMappings[sheetIdx].mappings[mappingIdx].sourceColumnIndex = sourceColIndex;
|
||||||
|
setSheetMappings(newMappings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBack={() => setStep("select_lists")}
|
||||||
|
onConsolidate={handleExecuteComparison}
|
||||||
|
isConsolidating={isComparing}
|
||||||
|
title="2. Spalten-Mapping für Vergleich prüfen"
|
||||||
|
description="Bitte überprüfe die gefundenen Kopfzeilen für alte und neue Liste."
|
||||||
|
actionLabel="Vergleichen"
|
||||||
|
actionLoadingLabel="Vergleich läuft..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// default: select_lists
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<h2>Listen vergleichen</h2>
|
||||||
|
<p>Lade Datei(en) hoch und wähle die alte und die neue Liste aus.</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
accept=".xlsx, .xls, .csv"
|
||||||
|
className={styles.fileInput}
|
||||||
|
id="compare-upload-file"
|
||||||
|
multiple
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<label htmlFor="compare-upload-file">
|
||||||
|
<Button appearance="secondary" as="a">
|
||||||
|
Externe Excel-Dateien laden
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<CardHeader header={<Label weight="semibold">1. Alte Liste (Basis)</Label>} />
|
||||||
|
<Dropdown
|
||||||
|
placeholder="Arbeitsblatt auswählen"
|
||||||
|
onOptionSelect={(_, data) => setOldSheetId(data.optionValue as string)}
|
||||||
|
value={availableSheets.find(s => s.id === oldSheetId)?.name || ""}
|
||||||
|
>
|
||||||
|
{renderOptions()}
|
||||||
|
</Dropdown>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<CardHeader header={<Label weight="semibold">2. Neue Liste (Aktueller Stand)</Label>} />
|
||||||
|
<Dropdown
|
||||||
|
placeholder="Arbeitsblatt auswählen"
|
||||||
|
onOptionSelect={(_, data) => setNewSheetId(data.optionValue as string)}
|
||||||
|
value={availableSheets.find(s => s.id === newSheetId)?.name || ""}
|
||||||
|
>
|
||||||
|
{renderOptions()}
|
||||||
|
</Dropdown>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
onClick={handleNextToMapping}
|
||||||
|
disabled={!oldSheetId || !newSheetId || oldSheetId === newSheetId}
|
||||||
|
>
|
||||||
|
Weiter zur Spaltenzuordnung
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompareWizard;
|
||||||
@@ -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".
|
||||||
@@ -115,20 +115,22 @@ function buildSheetMappingStatus(sheetInfo: SheetInfo, headerRow: any[], rowInde
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
sheetId: sheetInfo.id,
|
||||||
sheetName: sheetInfo.name, // Für Anzeige
|
sheetName: sheetInfo.name, // Für Anzeige
|
||||||
headerRowIndex: rowIndex,
|
headerRowIndex: rowIndex,
|
||||||
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 +189,769 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export interface ColumnMappingInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SheetMappingStatus {
|
export interface SheetMappingStatus {
|
||||||
|
sheetId?: string; // Eindeutige ID (z.B. für identisch benannte externe Blätter)
|
||||||
sheetName: string;
|
sheetName: string;
|
||||||
headerRowIndex: number; // 0-indexed
|
headerRowIndex: number; // 0-indexed
|
||||||
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 +27,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"] },
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
React container div.
|
React container div.
|
||||||
-->
|
-->
|
||||||
<div id="tridentmessage" style="display: none; padding: 10;">
|
<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)
|
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.
|
or to a Microsoft 365 account.
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
var normalContainer = document.getElementById("container");
|
var normalContainer = document.getElementById("container");
|
||||||
tridentMessage.style.display = "block";
|
tridentMessage.style.display = "block";
|
||||||
normalContainer.style.display = "none";
|
normalContainer.style.display = "none";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
BIN
tsc_output.txt
@@ -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,
|
||||||
|
|||||||