Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b14c36df0 | ||
|
|
3e444ae6a2 | ||
|
|
6410a28109 | ||
|
|
812d76e44c | ||
|
|
ae90228011 |
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
Binary file not shown.
@@ -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".
|
||||
|
||||
|
||||
16
deploy_docs.ps1
Normal file
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
docs/README.md
BIN
docs/README.md
Binary file not shown.
@@ -1,14 +1,14 @@
|
||||
<?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">
|
||||
<Id>2c37abde-33e4-4624-b95a-a0aed1526f1b</Id>
|
||||
<Version>1.0.1.1</Version>
|
||||
<Version>1.0.2.0</Version>
|
||||
<ProviderName>SAT Elektrotechnik GmbH</ProviderName>
|
||||
<DefaultLocale>de-DE</DefaultLocale>
|
||||
<DisplayName DefaultValue="SAT - Kabelliste Generator (PROD)"/>
|
||||
<Description DefaultValue="Konsolidiert strukturierte Kabelzugdaten aus mehreren Tabellenblättern in eine Gesamtliste."/>
|
||||
<IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/>
|
||||
<HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
|
||||
<SupportUrl DefaultValue="https://localhost:3000/"/>
|
||||
<SupportUrl DefaultValue="https://docs.casademm.de/"/>
|
||||
<AppDomains>
|
||||
<AppDomain>https://localhost:3000/</AppDomain>
|
||||
</AppDomains>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?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">
|
||||
<Id>2c37abde-33e4-4624-b95a-a0aed1526f1b</Id>
|
||||
<Version>1.0.1.1</Version>
|
||||
<Version>1.0.2.0</Version>
|
||||
<ProviderName>SAT Elektrotechnik GmbH</ProviderName>
|
||||
<DefaultLocale>de-DE</DefaultLocale>
|
||||
<DisplayName DefaultValue="SAT - Kabelliste Generator (DEV)"/>
|
||||
|
||||
25
mkdocs.yml
Normal file
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",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-React.git"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { makeStyles } from "@fluentui/react-components";
|
||||
import { makeStyles, TabList, Tab } from "@fluentui/react-components";
|
||||
import { SheetInfo, SheetMappingStatus, ConsolidateSettings } from "../models";
|
||||
import SheetSelector from "./SheetSelector";
|
||||
import ColumnMapper from "./ColumnMapper";
|
||||
import StatusNotifier from "./StatusNotifier";
|
||||
import CompareWizard from "./CompareWizard";
|
||||
import { getAvailableSheets, detectHeadersAndColumns, detectHeadersForSingleSheetRow, consolidateData } from "../excelLogic";
|
||||
|
||||
interface AppProps {
|
||||
@@ -24,6 +25,9 @@ const useStyles = makeStyles({
|
||||
const App: React.FC<AppProps> = () => {
|
||||
const styles = useStyles();
|
||||
|
||||
type AppMode = "consolidate" | "compare";
|
||||
const [appMode, setAppMode] = useState<AppMode>("consolidate");
|
||||
|
||||
type WizardStep = "select_sheets" | "map_columns" | "done";
|
||||
|
||||
const [step, setStep] = useState<WizardStep>("select_sheets");
|
||||
@@ -70,24 +74,24 @@ const App: React.FC<AppProps> = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleHeaderRowChange = async (sheetName: string, newRowIndex: number) => {
|
||||
const handleHeaderRowChange = async (sheetId: string, newRowIndex: number) => {
|
||||
try {
|
||||
// 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;
|
||||
|
||||
// Re-detect columns for exactly this row
|
||||
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) {
|
||||
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 => {
|
||||
if (sheet.sheetName !== sheetName) return sheet;
|
||||
if ((sheet.sheetId || sheet.sheetName) !== sheetId) return sheet;
|
||||
|
||||
const newMappings = sheet.mappings.map(m =>
|
||||
m.targetColumn === targetCol ? { ...m, sourceColumnIndex: sourceColIndex } : m
|
||||
@@ -119,53 +123,76 @@ const App: React.FC<AppProps> = () => {
|
||||
DEV ENVIRONMENT
|
||||
</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} />
|
||||
|
||||
{step === "select_sheets" && (
|
||||
<SheetSelector
|
||||
sheets={sheets}
|
||||
selectedSheetIds={selectedSheetIds}
|
||||
onSelectionChange={setSelectedSheetIds}
|
||||
onNext={handleNextToMapping}
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Footer Area with Links and Copyright */}
|
||||
<div style={{ marginTop: "auto", padding: "10px", textAlign: "center", fontSize: "12px", borderTop: "1px solid #eee", width: "100%", boxSizing: "border-box" }}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<a href="/manifest.prod.xml" download style={{ color: "#0078d4", textDecoration: "none", marginRight: "16px" }}>Prod-Manifest (.xml)</a>
|
||||
<a href="/docs/" target="_blank" rel="noopener noreferrer" style={{ color: "#0078d4", textDecoration: "none" }}>Dokumentation</a>
|
||||
<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.
|
||||
|
||||
@@ -17,13 +17,17 @@ import { SheetMappingStatus, TARGET_COLUMNS, ConsolidateSettings } from "../mode
|
||||
|
||||
interface ColumnMapperProps {
|
||||
sheetMappings: SheetMappingStatus[];
|
||||
onHeaderRowChange: (sheetName: string, newRowIndex: number) => void;
|
||||
onMappingChange: (sheetName: string, targetCol: string, sourceColIndex: number) => void;
|
||||
onHeaderRowChange: (sheetIdentifier: string, newRowIndex: number) => void;
|
||||
onMappingChange: (sheetIdentifier: string, targetCol: string, sourceColIndex: number) => void;
|
||||
onBack: () => void;
|
||||
onConsolidate: () => void;
|
||||
isConsolidating: boolean;
|
||||
settings: ConsolidateSettings;
|
||||
onSettingsChange: (settings: ConsolidateSettings) => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
actionLabel?: string;
|
||||
actionLoadingLabel?: string;
|
||||
}
|
||||
|
||||
const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
||||
@@ -35,22 +39,27 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
||||
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 (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "15px", padding: "10px" }}>
|
||||
<Text size={400} weight="semibold">
|
||||
2. Spalten-Mapping prüfen
|
||||
{title}
|
||||
</Text>
|
||||
<Text size={300}>
|
||||
Bitte überprüfe die gefundenen Kopfzeilen und passe fehlende Spalten manuell an.
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
<Accordion multiple collapsible defaultOpenItems={sheetMappings.map((s) => s.sheetName)}>
|
||||
<Accordion multiple collapsible defaultOpenItems={sheetMappings.map((s) => s.sheetId || s.sheetName)}>
|
||||
{sheetMappings.map((sheet) => {
|
||||
const identifier = sheet.sheetId || sheet.sheetName;
|
||||
const missingCount = sheet.mappings.filter((m) => m.sourceColumnIndex === -1).length;
|
||||
|
||||
return (
|
||||
<AccordionItem key={sheet.sheetName} value={sheet.sheetName}>
|
||||
<AccordionItem key={identifier} value={identifier}>
|
||||
<AccordionHeader>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
|
||||
<Text weight="semibold">{sheet.sheetName}</Text>
|
||||
@@ -79,7 +88,7 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
||||
min={0}
|
||||
onChange={(_, data) => {
|
||||
if (data.value !== undefined) {
|
||||
onHeaderRowChange(sheet.sheetName, data.value);
|
||||
onHeaderRowChange(identifier, data.value);
|
||||
}
|
||||
}}
|
||||
style={{ width: "80px" }}
|
||||
@@ -102,7 +111,7 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
||||
selectedOptions={[selectedVal || "-1"]}
|
||||
onOptionSelect={(_, data) => {
|
||||
const newIndex = parseInt(data.optionValue || "-1", 10);
|
||||
onMappingChange(sheet.sheetName, colName, newIndex);
|
||||
onMappingChange(identifier, colName, newIndex);
|
||||
}}
|
||||
style={{ minWidth: "150px" }}
|
||||
>
|
||||
@@ -169,7 +178,7 @@ const ColumnMapper: React.FC<ColumnMapperProps> = ({
|
||||
disabled={isConsolidating || sheetMappings.length === 0}
|
||||
icon={isConsolidating ? <Spinner size="tiny" /> : undefined}
|
||||
>
|
||||
{isConsolidating ? "Konsolidierung läuft..." : "Konsolidieren"}
|
||||
{isConsolidating ? actionLoadingLabel : actionLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
221
src/taskpane/components/CompareWizard.tsx
Normal file
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;
|
||||
@@ -115,6 +115,7 @@ function buildSheetMappingStatus(sheetInfo: SheetInfo, headerRow: any[], rowInde
|
||||
});
|
||||
|
||||
return {
|
||||
sheetId: sheetInfo.id,
|
||||
sheetName: sheetInfo.name, // Für Anzeige
|
||||
headerRowIndex: rowIndex,
|
||||
availableColumns,
|
||||
@@ -424,3 +425,533 @@ export async function consolidateData(mappings: SheetMappingStatus[], settings:
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
|
||||
* 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;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ColumnMappingInfo {
|
||||
}
|
||||
|
||||
export interface SheetMappingStatus {
|
||||
sheetId?: string; // Eindeutige ID (z.B. für identisch benannte externe Blätter)
|
||||
sheetName: string;
|
||||
headerRowIndex: number; // 0-indexed
|
||||
mappings: ColumnMappingInfo[];
|
||||
|
||||
BIN
tsc_output.txt
BIN
tsc_output.txt
Binary file not shown.
Reference in New Issue
Block a user