Update
This commit is contained in:
1 parent
c89d4825e2
commit
62794f72f1
8 files changed
+402
-78
No files matched your search
+55
-12
@@ -343,12 +343,24 @@ function sanitizeZammadArticleHtml(value: string) {
|
||||
return `${tags.open}${tag.replace(/^<span\b[^>]*>/i, "").replace(/<\/span>$/i, "")}${tags.close}`;
|
||||
});
|
||||
|
||||
const allowedArticleTags = new Set(["br", "div", "p", "span", "b", "strong", "i", "em", "u", "s", "strike", "ul", "ol", "li", "blockquote", "pre", "code", "h1", "h2", "h3"]);
|
||||
const sanitized = normalizedFormatting
|
||||
.replace(/<\s*(\/?)\s*(br|div|p|span|b|strong|i|em|u|s|strike|ul|ol|li|blockquote|pre|code|h1|h2|h3)\b[^>]*>/gi, (_tag, closing: string, rawName: string) => {
|
||||
.replace(/<[^>]*>/g, (tag) => {
|
||||
const match = /^<\s*(\/?)\s*([a-z][a-z0-9]*)\b[^>]*\/?\s*>$/i.exec(tag);
|
||||
|
||||
if (!match) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const [, closing, rawName] = match;
|
||||
const name = rawName.toLowerCase();
|
||||
|
||||
if (!allowedArticleTags.has(name)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return name === "br" ? "<br>" : `<${closing ? "/" : ""}${name}>`;
|
||||
})
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(new RegExp(`${imagePlaceholderPrefix}(\\d+)_`, "g"), (_placeholder, index: string) => images[Number(index)] ?? "");
|
||||
|
||||
if (!htmlToPlainText(sanitized) && images.length === 0) {
|
||||
@@ -790,7 +802,13 @@ async function getZammadCredentialsForUser(userId: string) {
|
||||
throw badRequest("Zammad URL muss zuerst im Adminbereich gespeichert werden");
|
||||
}
|
||||
|
||||
const apiToken = decryptSecret(userResult.rows[0]?.zammad_api_token);
|
||||
let apiToken: string | null = null;
|
||||
|
||||
try {
|
||||
apiToken = decryptSecret(userResult.rows[0]?.zammad_api_token);
|
||||
} catch {
|
||||
throw badRequest("Der gespeicherte Zammad-Token kann nicht entschlüsselt werden. Bitte den persönlichen Zammad-Token im Profil neu speichern.");
|
||||
}
|
||||
|
||||
if (!apiToken) {
|
||||
throw badRequest("Zammad-Token muss zuerst im Profil hinterlegt werden");
|
||||
@@ -874,6 +892,7 @@ async function zammadTimeUnitForUser(userId: string, durationSeconds: number) {
|
||||
type ZammadSessionArticlePayload = {
|
||||
ticket_id: number;
|
||||
type: "note";
|
||||
sender: "Agent";
|
||||
internal: true;
|
||||
subject: string;
|
||||
body: string;
|
||||
@@ -898,6 +917,7 @@ function zammadSessionArticlePayload(input: {
|
||||
return {
|
||||
ticket_id: ticketId,
|
||||
type: "note",
|
||||
sender: "Agent",
|
||||
internal: true,
|
||||
subject: "TicketTracker Session",
|
||||
body: input.body,
|
||||
@@ -912,7 +932,7 @@ function zammadSessionArticleBodyForDelivery(body: string, contentType: "text/pl
|
||||
return body;
|
||||
}
|
||||
|
||||
return `<div style="white-space: pre-wrap;">${body}</div>`;
|
||||
return /<\s*[a-z][\s/>]/i.test(body) ? body : `<div>${body}</div>`;
|
||||
}
|
||||
|
||||
async function postZammadSessionArticleForUser(userId: string, payload: ZammadSessionArticlePayload) {
|
||||
@@ -1077,6 +1097,13 @@ async function postImmediateZammadSessionArticle(input: {
|
||||
timeUnit: input.timeUnit,
|
||||
articleTypeId: input.articleTypeId
|
||||
});
|
||||
logDebug("zammad-session-article-post", {
|
||||
sessionId: input.sessionId,
|
||||
contentType: input.articleContentType,
|
||||
sourceBodyLength: input.articleBody.length,
|
||||
deliveryBodyLength: payload.body.length,
|
||||
deliveryBodyHasHtml: payload.body.includes("<")
|
||||
});
|
||||
const zammadResponse = await postZammadSessionArticleForUser(input.userId, payload);
|
||||
const articleId = zammadArticleId(zammadResponse);
|
||||
|
||||
@@ -1278,6 +1305,10 @@ async function sendZammadSessionArticleForSession(userId: string, sessionId: num
|
||||
throw badRequest("Ohne Tätigkeit kann keine Zammad-Notiz gesendet werden");
|
||||
}
|
||||
|
||||
if (!session.activity_html) {
|
||||
throw badRequest("Diese Session hat keinen HTML-Notizinhalt. Bitte Session bearbeiten und speichern, dann erneut an Zammad übertragen.");
|
||||
}
|
||||
|
||||
if (session.source_article_id !== null) {
|
||||
throw badRequest("Importierte Zammad-Mails können nicht erneut als Zammad-Notiz gesendet werden");
|
||||
}
|
||||
@@ -1292,8 +1323,8 @@ async function sendZammadSessionArticleForSession(userId: string, sessionId: num
|
||||
}
|
||||
|
||||
const timeUnit = await zammadTimeUnitForUser(userId, Number(session.duration_seconds));
|
||||
const articleBody = session.activity_html ?? session.activity;
|
||||
const articleContentType = session.activity_html ? "text/html" as const : "text/plain" as const;
|
||||
const articleBody = session.activity_html;
|
||||
const articleContentType = "text/html" as const;
|
||||
const articleId = await postImmediateZammadSessionArticle({
|
||||
userId,
|
||||
sessionId: session.session_id,
|
||||
@@ -1610,6 +1641,15 @@ async function createSessionForUser(input: SessionCreationInput) {
|
||||
throw Object.assign(new Error("Monat ist abgeschlossen"), { status: 409 });
|
||||
}
|
||||
|
||||
if (createZammadArticle && !zammadArticleHtml) {
|
||||
logDebug("zammad-session-article-missing-html", {
|
||||
userId,
|
||||
ticketNumber,
|
||||
activityLength: activity.length
|
||||
});
|
||||
throw badRequest("HTML-Inhalt der Zammad-Notiz fehlt. Bitte Seite neu laden und erneut speichern.");
|
||||
}
|
||||
|
||||
const roundedMinutes = Math.max(1, Math.round(durationSeconds / 60));
|
||||
const existingLocalTicket = useZammadLookup ? await ticketExists(ticketNumber) : false;
|
||||
const zammadTicket = providedZammadTicket !== undefined
|
||||
@@ -1800,9 +1840,9 @@ async function createSessionForUser(input: SessionCreationInput) {
|
||||
);
|
||||
const session = sessionResult.rows[0];
|
||||
|
||||
const zammadArticleBody = zammadArticleHtml ?? activity;
|
||||
const zammadArticleContentType = zammadArticleHtml ? "text/html" as const : "text/plain" as const;
|
||||
const shouldCreateZammadArticle = createZammadArticle && (activity.trim().length > 0 || Boolean(zammadArticleHtml));
|
||||
const zammadArticleBody = zammadArticleHtml ?? "";
|
||||
const zammadArticleContentType = "text/html" as const;
|
||||
const shouldCreateZammadArticle = createZammadArticle && Boolean(zammadArticleHtml);
|
||||
const zammadArticle: {
|
||||
requested: boolean;
|
||||
status: "not_requested" | "sent" | "failed";
|
||||
@@ -5411,12 +5451,15 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
|
||||
s.activity,
|
||||
s.activity_html AS zammad_article_body,
|
||||
s.work_type,
|
||||
s.project_teamspace_entry_id::text AS project_teamspace_entry_id,
|
||||
COALESCE(NULLIF(pte.teamspace_description, ''), pte.teamspace_project) AS project_teamspace_project,
|
||||
s.started_at,
|
||||
s.rounded_minutes,
|
||||
s.recurring_billing_id::text AS recurring_billing_id
|
||||
FROM sessions s
|
||||
JOIN tickets t ON t.id = s.ticket_id
|
||||
LEFT JOIN organizations so ON so.zammad_id = s.organization_id
|
||||
LEFT JOIN project_teamspace_entries pte ON pte.id = s.project_teamspace_entry_id
|
||||
WHERE s.started_at >= $1::timestamptz
|
||||
AND s.started_at < $2::timestamptz
|
||||
AND s.user_id = $3
|
||||
@@ -5504,7 +5547,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string, perio
|
||||
COALESCE(so.name, ot.name, s.customer_name, t.customer_name, 'Keine Organisation') AS organization_name,
|
||||
s.activity,
|
||||
s.work_type,
|
||||
COALESCE(pte.teamspace_project, p.teamspace_project) AS project_teamspace_project,
|
||||
COALESCE(NULLIF(pte.teamspace_description, ''), pte.teamspace_project, p.teamspace_project) AS project_teamspace_project,
|
||||
s.started_at,
|
||||
(s.started_at AT TIME ZONE 'Europe/Berlin')::date AS day,
|
||||
s.rounded_minutes,
|
||||
@@ -6046,7 +6089,7 @@ async function getPeriodTicket(config: PeriodConfig, period: ParsedPeriod, ticke
|
||||
) AS zammad_article_type_id,
|
||||
project_teamspace_entry_id::text AS project_teamspace_entry_id,
|
||||
(
|
||||
SELECT teamspace_project
|
||||
SELECT COALESCE(NULLIF(teamspace_description, ''), teamspace_project)
|
||||
FROM project_teamspace_entries
|
||||
WHERE id = sessions.project_teamspace_entry_id
|
||||
) AS project_teamspace_project
|
||||
@@ -6208,7 +6251,7 @@ async function getTicketDetails(ticketId: number, userId: string, filters: { mon
|
||||
za.zammad_article_id::text AS zammad_article_id,
|
||||
za.zammad_article_type_id,
|
||||
s.project_teamspace_entry_id::text AS project_teamspace_entry_id,
|
||||
pte.teamspace_project AS project_teamspace_project
|
||||
COALESCE(NULLIF(pte.teamspace_description, ''), pte.teamspace_project) AS project_teamspace_project
|
||||
FROM sessions s
|
||||
LEFT JOIN project_teamspace_entries pte ON pte.id = s.project_teamspace_entry_id
|
||||
LEFT JOIN LATERAL (
|
||||
|
||||
Reference in new issue
Block a user