starface-outlook-sync-addin/src/taskpane/components/ProfileEditor.tsx

356 lines
9.7 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
PrimaryButton,
DefaultButton,
TextField,
Dropdown,
IDropdownOption,
Stack,
Text,
Toggle,
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
} from "@fluentui/react";
import {
SyncProfile,
StarfaceConnection,
StarfaceAddressBook,
OutlookContactFolder,
} from "../../models/types";
import { ProfileManager } from "../../services/profile-manager";
import { SyncEngine } from "../../services/sync-engine";
interface Props {
profile: SyncProfile | null;
onSave: () => void;
onCancel: () => void;
}
const defaultConnection: StarfaceConnection = {
host: "",
port: 443,
useSsl: true,
loginId: "",
password: "",
};
export const ProfileEditor: React.FC<Props> = ({
profile,
onSave,
onCancel,
}) => {
const pm = new ProfileManager();
const engine = new SyncEngine();
const isNew = !profile;
const [name, setName] = useState(profile?.name || "");
const [connection, setConnection] = useState<StarfaceConnection>(
profile?.starfaceConnection || { ...defaultConnection }
);
const [addressBooks, setAddressBooks] = useState<StarfaceAddressBook[]>([]);
const [selectedBook, setSelectedBook] = useState<StarfaceAddressBook | null>(
profile?.starfaceAddressBook || null
);
const [outlookFolders, setOutlookFolders] = useState<OutlookContactFolder[]>(
[]
);
const [selectedFolderId, setSelectedFolderId] = useState(
profile?.outlookFolderId || ""
);
const [syncDirection, setSyncDirection] = useState<SyncProfile["syncDirection"]>(
profile?.syncDirection || "both"
);
const [enabled, setEnabled] = useState(profile?.enabled ?? true);
const [loading, setLoading] = useState(false);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
const [error, setError] = useState("");
// Load Outlook folders on mount
useEffect(() => {
loadOutlookFolders();
}, []);
const loadOutlookFolders = async () => {
try {
const folders = await engine.loadOutlookFolders();
setOutlookFolders(folders);
if (!selectedFolderId && folders.length > 0) {
setSelectedFolderId(folders[0].id);
}
} catch (err) {
console.error("Failed to load Outlook folders:", err);
// Provide a default folder option
setOutlookFolders([{ id: "default", displayName: "Kontakte (Standard)" }]);
if (!selectedFolderId) setSelectedFolderId("default");
}
};
const handleTestConnection = async () => {
setLoading(true);
setTestResult(null);
const result = await engine.testStarfaceConnection(connection);
setTestResult(result);
if (!result.success && result.message.toLowerCase().includes("fetch")) {
result.message +=
"\n\nMögliche Ursache: Das SSL-Zertifikat der Starface ist nicht vertrauenswürdig. " +
"Bitte als Administrator ausführen:\n" +
`import-cert.ps1 -StarfaceHost ${connection.host} -Port ${connection.port}`;
}
setLoading(false);
};
const handleLoadAddressBooks = async () => {
setLoading(true);
setError("");
try {
const books = await engine.loadStarfaceAddressBooks(connection);
setAddressBooks(books);
if (books.length > 0 && !selectedBook) {
setSelectedBook(books[0]);
}
if (books.length === 0) {
setError("Keine Adressbücher gefunden");
}
} catch (err) {
setError(`Fehler: ${err}`);
}
setLoading(false);
};
const handleSave = () => {
if (!name.trim()) {
setError("Bitte einen Profilnamen eingeben");
return;
}
if (!connection.host.trim()) {
setError("Bitte Starface-Host eingeben");
return;
}
if (!selectedBook) {
setError("Bitte ein Starface-Adressbuch auswählen");
return;
}
if (!selectedFolderId) {
setError("Bitte einen Outlook-Ordner auswählen");
return;
}
const selectedFolder = outlookFolders.find(
(f) => f.id === selectedFolderId
);
const newProfile: SyncProfile = {
id: profile?.id || pm.generateId(),
name: name.trim(),
starfaceConnection: connection,
starfaceAddressBook: selectedBook,
outlookFolderId: selectedFolderId,
outlookFolderName:
selectedFolder?.displayName || "Kontakte",
syncDirection,
lastSync: profile?.lastSync,
enabled,
};
if (isNew) {
pm.addProfile(newProfile);
} else {
pm.updateProfile(newProfile);
}
onSave();
};
const directionOptions: IDropdownOption[] = [
{ key: "both", text: "Bidirektional (↔)" },
{ key: "outlook-to-starface", text: "Outlook → Starface" },
{ key: "starface-to-outlook", text: "Starface → Outlook" },
];
const bookOptions: IDropdownOption[] = addressBooks.map((b, i) => ({
key: i.toString(),
text: b.name,
data: b,
}));
const folderOptions: IDropdownOption[] = outlookFolders.map((f) => ({
key: f.id,
text: f.displayName,
}));
return (
<div className="profile-editor">
<Stack tokens={{ childrenGap: 16 }}>
<Stack horizontal horizontalAlign="space-between" verticalAlign="center">
<Text variant="xLarge">
{isNew ? "Neues Profil" : "Profil bearbeiten"}
</Text>
<DefaultButton text="Zurück" onClick={onCancel} />
</Stack>
{error && (
<MessageBar
messageBarType={MessageBarType.error}
onDismiss={() => setError("")}
>
{error}
</MessageBar>
)}
<TextField
label="Profilname"
value={name}
onChange={(_, v) => setName(v || "")}
placeholder="z.B. Firmenkontakte Hauptanlage"
required
/>
<Text variant="large" className="section-title">
Starface-Verbindung
</Text>
<Stack horizontal tokens={{ childrenGap: 8 }}>
<Stack.Item grow>
<TextField
label="Host / IP-Adresse"
value={connection.host}
onChange={(_, v) =>
setConnection({ ...connection, host: v || "" })
}
placeholder="z.B. pbx.firma.de oder 192.168.1.100"
required
/>
</Stack.Item>
<TextField
label="Port"
value={connection.port.toString()}
onChange={(_, v) =>
setConnection({ ...connection, port: parseInt(v || "443") || 443 })
}
styles={{ root: { width: 80 } }}
/>
</Stack>
<Toggle
label="HTTPS verwenden"
checked={connection.useSsl}
onChange={(_, checked) =>
setConnection({
...connection,
useSsl: checked ?? true,
port: checked ? 443 : 80,
})
}
/>
<TextField
label="Login-ID"
value={connection.loginId}
onChange={(_, v) =>
setConnection({ ...connection, loginId: v || "" })
}
required
/>
<TextField
label="Kennwort"
type="password"
value={connection.password}
onChange={(_, v) =>
setConnection({ ...connection, password: v || "" })
}
canRevealPassword
required
/>
<Stack horizontal tokens={{ childrenGap: 8 }}>
<DefaultButton
text="Verbindung testen"
onClick={handleTestConnection}
disabled={loading || !connection.host || !connection.loginId}
/>
<PrimaryButton
text="Adressbücher laden"
onClick={handleLoadAddressBooks}
disabled={loading || !connection.host || !connection.loginId}
/>
{loading && <Spinner size={SpinnerSize.small} />}
</Stack>
{testResult && (
<MessageBar
messageBarType={
testResult.success
? MessageBarType.success
: MessageBarType.error
}
>
{testResult.message.split("\n").map((line, i) => (
<span key={i}>
{i > 0 && <br />}
{line}
</span>
))}
</MessageBar>
)}
{addressBooks.length > 0 && (
<Dropdown
label="Starface-Adressbuch"
options={bookOptions}
selectedKey={
selectedBook
? addressBooks.indexOf(selectedBook).toString()
: undefined
}
onChange={(_, option) => {
if (option?.data) setSelectedBook(option.data);
}}
required
/>
)}
<Text variant="large" className="section-title">
Outlook-Einstellungen
</Text>
<Dropdown
label="Outlook Kontakte-Ordner"
options={folderOptions}
selectedKey={selectedFolderId}
onChange={(_, option) => {
if (option) setSelectedFolderId(option.key as string);
}}
required
/>
<Dropdown
label="Synchronisationsrichtung"
options={directionOptions}
selectedKey={syncDirection}
onChange={(_, option) => {
if (option) setSyncDirection(option.key as SyncProfile["syncDirection"]);
}}
/>
<Toggle
label="Profil aktiviert"
checked={enabled}
onChange={(_, checked) => setEnabled(checked ?? true)}
/>
<Stack horizontal tokens={{ childrenGap: 8 }}>
<PrimaryButton text="Speichern" onClick={handleSave} />
<DefaultButton text="Abbrechen" onClick={onCancel} />
</Stack>
</Stack>
</div>
);
};