This commit is contained in:
michael committed 2026-10-06 08:57:37 +02:00
1 parent e1456ed4ca
commit fc0dfd0556
3 files changed
+85 -23

No files matched your search

+74 -20
View File
@@ -4230,6 +4230,16 @@ function addSyncChange(changes: ZammadSyncChange[], field: string, from: unknown
}
}
const informationalZammadSyncChangeFields = new Set([
"Fremdes Ticket",
"Fremder Zammad-Owner",
"Hinweis"
]);
function zammadSyncResultChanges(changes: ZammadSyncChange[]) {
return changes.some((change) => !informationalZammadSyncChangeFields.has(change.field)) ? changes : [];
}
function zammadSyncSnapshot(zammadTicket: ZammadTicketImport) {
return {
ticketId: zammadTicket.zammadTicketId,
@@ -5161,7 +5171,9 @@ async function planExistingZammadTicketSync(
}
if (zammadTicket.workType === "support" && zammadTicket.zammadStateId === 4) {
return changes.length > 0
const resultChanges = zammadSyncResultChanges(changes);
return resultChanges.length > 0
? {
ticketNumber: localTicket.ticket_number,
action: "update",
@@ -5172,7 +5184,7 @@ async function planExistingZammadTicketSync(
: "Zammad-Metadaten werden aktualisiert.",
local: localSyncSnapshot(localTicket, null, { zammadPendingTodoExists: pendingTodoExists }),
zammad: zammadSyncSnapshot(zammadTicket),
changes
changes: resultChanges
}
: null;
}
@@ -5208,17 +5220,19 @@ async function planExistingZammadTicketSync(
changes.push({ field: "Projekt/Teamspace-Daten", from: "vorhanden", to: "werden gelöscht" });
}
const resultChanges = zammadSyncResultChanges(changes);
return {
ticketNumber: localTicket.ticket_number,
action: changes.length > 0 ? "update" : "skip",
action: resultChanges.length > 0 ? "update" : "skip",
reason: zammadTicket.ticketNumber !== localTicket.ticket_number
? mergedTargetTicket
? "Zammad-Merge wird lokal nachvollzogen; vorhandenes Ziel-Ticket wird verwendet."
: "Zammad-Merge wird lokal nachvollzogen; Ziel-Ticket wird synchronisiert und die Quelle umgehängt."
: changes.length > 0 ? "Ticket wird als Support synchronisiert." : "Keine Änderung nach aktueller Auswahl.",
: resultChanges.length > 0 ? "Ticket wird als Support synchronisiert." : "Keine Änderung nach aktueller Auswahl.",
local: localSyncSnapshot(localTicket, null, { zammadPendingTodoExists: pendingTodoExists }),
zammad: zammadSyncSnapshot(zammadTicket),
changes
changes: resultChanges
};
}
@@ -5263,13 +5277,15 @@ async function planExistingZammadTicketSync(
}
const resultChanges = zammadSyncResultChanges(changes);
return {
ticketNumber: localTicket.ticket_number,
action: changes.length > 0 ? "update" : "skip",
reason: changes.length > 0 ? "Ticket wird aktualisiert; Projektdaten bleiben wegen offener TS/TT-Prüfung lokal geschützt." : "Keine Änderung nach aktueller Auswahl.",
action: resultChanges.length > 0 ? "update" : "skip",
reason: resultChanges.length > 0 ? "Ticket wird aktualisiert; Projektdaten bleiben wegen offener TS/TT-Prüfung lokal geschützt." : "Keine Änderung nach aktueller Auswahl.",
local: localSyncSnapshot(localTicket, project, { zammadPendingTodoExists: pendingTodoExists }),
zammad: zammadSyncSnapshot(zammadTicket),
changes
changes: resultChanges
};
}
@@ -5277,7 +5293,7 @@ async function planExistingZammadTicketSync(
return null;
}
if (!project) {
if (!project && fields.type && localTicket.work_type !== "consulting") {
changes.push({ field: "Projekt", from: null, to: "wird angelegt" });
}
@@ -5327,17 +5343,19 @@ async function planExistingZammadTicketSync(
}
}
const resultChanges = zammadSyncResultChanges(changes);
return {
ticketNumber: localTicket.ticket_number,
action: changes.length > 0 ? "update" : "skip",
action: resultChanges.length > 0 ? "update" : "skip",
reason: zammadTicket.ticketNumber !== localTicket.ticket_number
? mergedTargetTicket
? "Zammad-Merge wird lokal nachvollzogen; vorhandenes Ziel-Ticket wird verwendet."
: "Zammad-Merge wird lokal nachvollzogen; Ziel-Ticket wird synchronisiert und die Quelle umgehängt."
: changes.length > 0 ? "Ticket wird aktualisiert." : "Keine Änderung nach aktueller Auswahl.",
? mergedTargetTicket
? "Zammad-Merge wird lokal nachvollzogen; vorhandenes Ziel-Ticket wird verwendet."
: "Zammad-Merge wird lokal nachvollzogen; Ziel-Ticket wird synchronisiert und die Quelle umgehängt."
: resultChanges.length > 0 ? "Ticket wird aktualisiert." : "Keine Änderung nach aktueller Auswahl.",
local: localSyncSnapshot(localTicket, project, { zammadPendingTodoExists: pendingTodoExists }),
zammad: zammadSyncSnapshot(zammadTicket),
changes
changes: resultChanges
};
}
@@ -6669,6 +6687,7 @@ async function applyZammadTicketSync(
const organization = shouldResolveOrganization ? await getOrganizationByZammadId(client, zammadTicket.organizationId) : null;
const ownerMapping = await zammadOwnerUserMapping(client, zammadTicket.zammadOwnerId);
const zammadOwnerUserId = ownerMapping.status === "ambiguous" ? localTicket.user_id : ownerMapping.userId;
const zammadStateChanged = numberOrNull(localTicket.zammad_state_id) !== zammadTicket.zammadStateId;
if (shouldResolveOrganization && !organization) {
return "organization-missing" as const;
@@ -6754,6 +6773,12 @@ async function applyZammadTicketSync(
);
}
if (zammadStateChanged) {
const completedAt = new Date();
await restartProjectFollowupPeriodForTicket(client, localTicket.id, completedAt);
await completeAutomaticProjectFollowupsForTicketStatusChange(client, localTicket.id, completedAt);
}
if (fields.pendingTodo) {
await ensureZammadPendingTodo(client, localTicket.id, userId, zammadTicket);
}
@@ -7778,6 +7803,35 @@ async function completeAutomaticProjectFollowupsForSession(client: QueryClient,
);
}
async function completeAutomaticProjectFollowupsForTicketStatusChange(client: QueryClient, ticketId: string, completedAt: Date) {
await client.query(
`
UPDATE ticket_todos tt
SET status = 'done',
completed_at = COALESCE(tt.completed_at, $2::timestamptz),
updated_at = now()
FROM project_followup_reminders pfr
WHERE pfr.todo_id = tt.id
AND pfr.ticket_id = $1
AND tt.ticket_id = $1
AND tt.status = 'open';
`,
[ticketId, completedAt.toISOString()]
);
}
async function restartProjectFollowupPeriodForTicket(client: QueryClient, ticketId: string, restartedAt: Date) {
await client.query(
`
UPDATE projects
SET updated_at = GREATEST(updated_at, $2::timestamptz)
WHERE ticket_id = $1
AND closed_at IS NULL;
`,
[ticketId, restartedAt.toISOString()]
);
}
async function getPeriodTicket(config: PeriodConfig, period: ParsedPeriod, ticketId: number, userId: string) {
await ensureRecurringSessionsForUserPeriod(userId, period.startIso, period.endIso);
@@ -12442,12 +12496,12 @@ app.patch("/api/projects/:projectId/review", requireUser, async (req, res) => {
const updateResult = await client.query<{ id: string; ticket_id: string; close_review_required: boolean }>(
`
UPDATE projects
SET close_review_required = NOT $3::boolean,
SET close_review_required = NOT $2::boolean,
updated_at = now()
WHERE id = $1
RETURNING id::text, ticket_id::text, close_review_required;
`,
[projectId, userId, acknowledged]
[projectId, acknowledged]
);
return updateResult.rows[0] ?? null;
@@ -12493,13 +12547,13 @@ app.patch("/api/projects/:projectId/status", requireUser, async (req, res) => {
const updateResult = await client.query<{ id: string; ticket_id: string; closed_at: string | null }>(
`
UPDATE projects
SET closed_at = CASE WHEN $3::boolean THEN now() ELSE NULL END,
close_review_required = CASE WHEN $3::boolean THEN false ELSE close_review_required END,
SET closed_at = CASE WHEN $2::boolean THEN now() ELSE NULL END,
close_review_required = CASE WHEN $2::boolean THEN false ELSE close_review_required END,
updated_at = now()
WHERE id = $1
RETURNING id::text, ticket_id::text, closed_at;
`,
[projectId, userId, closed]
[projectId, closed]
);
return updateResult.rows[0] ?? null;