Final
This commit is contained in:
1 parent
fc0dfd0556
commit
649dd6f028
20 files changed
+3725
-988
No files matched your search
@@ -17,7 +17,9 @@ import {
|
||||
previewExistingZammadTickets,
|
||||
previewOwnZammadConsultingTickets,
|
||||
previewZammadMailSessions,
|
||||
syncTeamspaceProjectIds,
|
||||
waitForZammadSyncJob,
|
||||
type TeamspaceProjectIdSyncResult,
|
||||
type ZammadSyncExistingFields,
|
||||
type ZammadSyncChange,
|
||||
type ZammadSyncJob,
|
||||
@@ -40,7 +42,7 @@ const defaultFields: ZammadSyncExistingFields = {
|
||||
organization: true,
|
||||
type: true,
|
||||
projectName: true,
|
||||
budget: true,
|
||||
budget: false,
|
||||
overrideBudget: false,
|
||||
pendingTodo: false
|
||||
};
|
||||
@@ -50,7 +52,6 @@ const fieldOptions: Array<{ key: Exclude<keyof ZammadSyncExistingFields, "overri
|
||||
{ key: "organization", label: "Organisation", description: "Lokale Organisation anhand der Zammad-ID setzen." },
|
||||
{ key: "type", label: "Typ", description: "Support/Consulting übernehmen, Projekte anlegen oder entfernen." },
|
||||
{ key: "projectName", label: "Projektbezeichnung", description: "Teamspace-Projekt bei Consulting überschreiben." },
|
||||
{ key: "budget", label: "Budget", description: "Teamspace-Stundenkontingent überschreiben." },
|
||||
{ key: "pendingTodo", label: "Termin-ToDo", description: "Aus Wartend/Abschluss mit Datum ein ToDo erzeugen." }
|
||||
];
|
||||
|
||||
@@ -429,10 +430,6 @@ function formatChangeValue(field: string, value: unknown) {
|
||||
return formatDurationSeconds(value);
|
||||
}
|
||||
|
||||
if (field === "Teamspace-Budget") {
|
||||
return formatMinutes(value);
|
||||
}
|
||||
|
||||
if (field === "Teamspace-Projekte" || field === "Teamspace-Prüfung" || field === "Merge-Projektdaten") {
|
||||
return formatTeamspaceProjectList(value) || "-";
|
||||
}
|
||||
@@ -498,7 +495,7 @@ function changeAllowedByFields(item: ZammadSyncItem, change: ZammadSyncChange, f
|
||||
}
|
||||
|
||||
if (change.field === "Teamspace-Budget") {
|
||||
return fields.budget && consultingProjectCanBeTouched;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (change.field === "ToDo") {
|
||||
@@ -711,7 +708,6 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
||||
const zammadTeamspaceProjects = cachedStringList(zammad, "teamspaceProjects");
|
||||
const localTeamspaceProjectList = localTeamspaceProjects.length ? localTeamspaceProjects : [cachedValue(project, "teamspaceProject")].filter((entry): entry is string => typeof entry === "string");
|
||||
const zammadTeamspaceProjectList = zammadTeamspaceProjects.length ? zammadTeamspaceProjects : splitTeamspaceProjects(cachedValue(zammad, "teamspaceProject"));
|
||||
const canSyncBudget = fields.budget && (cachedValue(project, "defaultBudgetSyncDisabled") !== true || fields.overrideBudget);
|
||||
const changes: ZammadSyncChange[] = [];
|
||||
|
||||
addCachedChange(changes, "Zammad-Priorität", cachedValue(local, "zammadPriorityId"), cachedValue(zammad, "priorityId"));
|
||||
@@ -753,10 +749,6 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
||||
addCachedTeamspaceProjectChanges(changes, localTeamspaceProjectList, zammadTeamspaceProjectList);
|
||||
}
|
||||
|
||||
if (canSyncBudget) {
|
||||
addCachedChange(changes, "Teamspace-Budget", cachedValue(project, "defaultHourBudgetMinutes") ?? cachedValue(project, "hourBudgetMinutes"), cachedValue(zammad, "teamspaceBudgetMinutes"));
|
||||
}
|
||||
|
||||
if (zammadStateId === 4 && cachedValue(project, "closedAt") === null && cachedValue(project, "closeReviewRequired") !== true) {
|
||||
changes.push({ field: "TS/TT-Prüfung", from: cachedValue(project, "closeReviewRequired") ?? false, to: true });
|
||||
}
|
||||
@@ -920,8 +912,7 @@ function zammadDetails(item: ZammadSyncItem) {
|
||||
`Owner ${formatValue(zammad.ownerId)}`,
|
||||
`Org ${formatValue(zammad.organizationId)}`,
|
||||
`Titel ${formatValue(zammad.title)}`,
|
||||
`Projekt ${formatValue(formatTeamspaceProjectList(zammad.teamspaceProjects) || zammad.teamspaceProject)}`,
|
||||
`Budget ${formatMinutes(zammad.teamspaceBudgetMinutes)}`
|
||||
`Projekt ${formatValue(formatTeamspaceProjectList(zammad.teamspaceProjects) || zammad.teamspaceProject)}`
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1105,11 +1096,14 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
syncingMail
|
||||
} = pageState;
|
||||
const [storageReady, setStorageReady] = useState(false);
|
||||
const [syncingTeamspaceProjectIds, setSyncingTeamspaceProjectIds] = useState(false);
|
||||
const [teamspaceProjectIdResult, setTeamspaceProjectIdResult] = useState<TeamspaceProjectIdSyncResult | null>(null);
|
||||
const displayedExistingResult = useMemo(() => applyFieldsToExistingPreview(existingResult, fields), [existingResult, fields]);
|
||||
const selectedExistingCount = useMemo(() => selectedTicketNumbers(displayedExistingResult, existingSelection).length, [displayedExistingResult, existingSelection]);
|
||||
const selectedImportCount = useMemo(() => selectedTicketNumbers(importResult, importSelection).length, [importResult, importSelection]);
|
||||
const selectedMailCount = useMemo(() => selectedArticleIds(mailResult, mailSelection).length, [mailResult, mailSelection]);
|
||||
const zammadReady = currentUser.has_zammad_api_token && Boolean(currentUser.zammad_user_id);
|
||||
const teamspaceReady = currentUser.has_teamspace_credentials && Boolean(currentUser.teamspace_user_id);
|
||||
|
||||
useEffect(() => {
|
||||
setStorageReady(false);
|
||||
@@ -1552,6 +1546,28 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runTeamspaceProjectIdSync() {
|
||||
if (syncingTeamspaceProjectIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncingTeamspaceProjectIds(true);
|
||||
|
||||
try {
|
||||
const { result } = await syncTeamspaceProjectIds();
|
||||
setTeamspaceProjectIdResult(result);
|
||||
toast.success("Teamspace-Projekt-IDs synchronisiert", {
|
||||
description: `${result.updated} aktualisiert, ${result.alreadyCurrent} bereits aktuell, ${result.subentriesSynced} Unterposition(en), ${result.missing + result.ambiguous} ohne eindeutige Zuordnung.`
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Teamspace-Projekt-IDs konnten nicht synchronisiert werden", {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
} finally {
|
||||
setSyncingTeamspaceProjectIds(false);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExisting(ticketNumber: string, checked: boolean) {
|
||||
setZammadSyncPageState((current) => ({
|
||||
existingSelection: { ...current.existingSelection, [ticketNumber]: checked }
|
||||
@@ -1617,6 +1633,105 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<DatabaseZap className="size-5 text-muted-foreground" />
|
||||
Teamspace-Projekt-IDs
|
||||
</CardTitle>
|
||||
<CardDescription>Offene Consulting-Projekte gegen die Teamspace-API auflösen und API-ID, Gesamtbudget sowie Unterpositionen aus Teamspace speichern.</CardDescription>
|
||||
</div>
|
||||
<Badge variant={teamspaceReady ? "success" : "secondary"}>{teamspaceReady ? "bereit" : "nicht bereit"}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 px-4 pb-4">
|
||||
{!teamspaceReady ? (
|
||||
<Alert variant="warning">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertTitle>Teamspace-Zugang fehlt</AlertTitle>
|
||||
<AlertDescription>Bitte im Profil Geräte-ID und Gerätepasswort speichern, damit die Teamspace User-ID bekannt ist.</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="flex justify-end">
|
||||
<Button type="button" disabled={!teamspaceReady || syncingTeamspaceProjectIds} onClick={() => void runTeamspaceProjectIdSync()}>
|
||||
<RefreshCw className="size-4" />
|
||||
{syncingTeamspaceProjectIds ? "Synchronisiert..." : "Teamspace-IDs, Budgets und Positionen synchronisieren"}
|
||||
</Button>
|
||||
</div>
|
||||
{teamspaceProjectIdResult ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-2 sm:grid-cols-3 xl:grid-cols-9">
|
||||
{[
|
||||
["TT-Einträge", teamspaceProjectIdResult.checkedEntries],
|
||||
["TS gelesen", teamspaceProjectIdResult.remoteProjectsScanned],
|
||||
["TS Positionen gelesen", teamspaceProjectIdResult.remoteSubprojectsScanned],
|
||||
["Treffer", teamspaceProjectIdResult.matched],
|
||||
["IDs aktualisiert", teamspaceProjectIdResult.updated],
|
||||
["Aktuell", teamspaceProjectIdResult.alreadyCurrent],
|
||||
["Positionen", teamspaceProjectIdResult.subentriesSynced],
|
||||
["Entfernt", teamspaceProjectIdResult.subentriesRemoved],
|
||||
["Offen", teamspaceProjectIdResult.missing + teamspaceProjectIdResult.ambiguous]
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="rounded-md border bg-muted/20 px-3 py-2">
|
||||
<span className="block text-xs text-muted-foreground">{label}</span>
|
||||
<span className="text-lg font-semibold">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{teamspaceProjectIdResult.duplicateRemoteProjectIds.length > 0 ? (
|
||||
<Alert variant="warning">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertTitle>Nicht eindeutige Teamspace projectIds</AlertTitle>
|
||||
<AlertDescription>{teamspaceProjectIdResult.duplicateRemoteProjectIds.slice(0, 8).join(", ")}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="max-h-80 overflow-auto rounded-md border">
|
||||
<Table className="w-full table-fixed">
|
||||
<colgroup>
|
||||
<col className="w-36" />
|
||||
<col className="w-44" />
|
||||
<col className="w-28" />
|
||||
<col className="w-32" />
|
||||
<col />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Ticket</TableHead>
|
||||
<TableHead>Teamspace-ID</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>API-ID</TableHead>
|
||||
<TableHead>Hinweis</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{teamspaceProjectIdResult.items.slice(0, 100).map((item) => (
|
||||
<TableRow key={item.entryId}>
|
||||
<TableCell className="truncate font-medium" title={item.ticketNumber}>{item.ticketNumber}</TableCell>
|
||||
<TableCell className="truncate" title={item.teamspaceProject}>{item.teamspaceProject}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={item.status === "updated" ? "success" : item.status === "current" ? "info" : "warning"}>
|
||||
{item.status === "updated" ? "Aktualisiert" : item.status === "current" ? "Aktuell" : item.status === "missing" ? "Fehlt" : "Mehrdeutig"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="truncate" title={item.apiProjectId ?? ""}>{item.apiProjectId ?? "-"}</TableCell>
|
||||
<TableCell className="truncate text-xs text-muted-foreground" title={item.message ?? item.apiProjectUrl ?? ""}>
|
||||
{item.message ?? item.apiProjectUrl ?? "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{teamspaceProjectIdResult.items.length > 100 ? (
|
||||
<p className="text-xs text-muted-foreground">Weitere {teamspaceProjectIdResult.items.length - 100} Einträge wurden synchronisiert und sind hier nicht gelistet.</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
@@ -1642,15 +1757,6 @@ export function ZammadSyncPage({ currentUser }: ZammadSyncPageProps) {
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{fields.budget ? (
|
||||
<label className={cn("flex min-w-0 gap-3 rounded-md border p-3", fields.overrideBudget ? "bg-muted/30" : "bg-background")}>
|
||||
<Checkbox checked={fields.overrideBudget} onCheckedChange={(checked) => setField("overrideBudget", checked === true)} />
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-medium">Budget-Schutz im Sync ignorieren</span>
|
||||
<span className="block text-xs text-muted-foreground">Aus: nur ungeschützte Budgets aktualisieren. An: Zammad überschreibt auch geschützte Budgets.</span>
|
||||
</span>
|
||||
</label>
|
||||
) : null}
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{syncingExisting ? (
|
||||
<Button type="button" variant="outline" onClick={resetRunningState}>
|
||||
|
||||
Reference in new issue
Block a user